/* ROS Logistic — components.

   Built once on the tokens in tokens.css. No colour, size, radius or spacing literal appears
   here: values are var(--…) or structural (0, %, fr, em relative to the tokenised type size,
   auto), and tests/common/test_tokens.py scans this file for a hex colour, an rgb or rgba
   function, or a px literal.
   Media queries are the one place a size has to be written out - custom properties cannot be
   read there - so the width query at the foot of this file carries DESIGN.md's `sm` breakpoint
   verbatim, and that line is the only place here where a size is not a token.

   Three surfaces, one stylesheet. Everything is styled for the light Back office and Portal;
   the query at the end rescales targets and columns for the Almacén (the staff chain below
   the `md` breakpoint), and tokens.css swaps the colours under the same query. Nothing reads a role. */

/* ── Reset ────────────────────────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  min-height: 100vh;
  margin: 0;
  display: flex;
  flex-direction: column;
  background: var(--color-surface-base);
  color: var(--color-ink);
  font-family: var(--font-sans);
  font-size: var(--text-body-size);
  font-weight: var(--text-body-weight);
  line-height: var(--text-body-line);
}
h1, h2, h3, h4, p, ul, ol, dl, figure, fieldset { margin: 0; }
fieldset { min-width: 0; padding: 0; border: 0; }
legend { padding: 0; }
img, svg { display: block; max-width: 100%; }
button, input, select, textarea { margin: 0; font: inherit; color: inherit; }
button { padding: 0; border: 0; background: none; text-align: inherit; cursor: pointer; }
a { color: inherit; }
summary { cursor: pointer; }

/* ── Type ramp (DESIGN.md typography) ─────────────────────────────────────────────────────── */

.display {
  font-size: var(--text-display-size);
  font-weight: var(--text-display-weight);
  line-height: var(--text-display-line);
  letter-spacing: var(--text-display-tracking);
}
h1, .title {
  font-size: var(--text-title-size);
  font-weight: var(--text-title-weight);
  line-height: var(--text-title-line);
  letter-spacing: var(--text-title-tracking);
}
h2, .heading {
  font-size: var(--text-heading-size);
  font-weight: var(--text-heading-weight);
  line-height: var(--text-heading-line);
}
.body-sm {
  font-size: var(--text-body-sm-size);
  font-weight: var(--text-body-sm-weight);
  line-height: var(--text-body-sm-line);
}
/* The only uppercase style in the product. Running text is never uppercased. */
.label {
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
  text-transform: uppercase;
}
/* Machine identifiers, m³, money, timestamps: 0 is not O, 1 is not l, columns stay still. */
.data {
  font-family: var(--font-mono);
  font-size: var(--text-data-size);
  font-weight: var(--text-data-weight);
  line-height: var(--text-data-line);
  font-variant-numeric: tabular-nums;
}
.data-lg {
  font-family: var(--font-mono);
  font-size: var(--text-data-lg-size);
  font-weight: var(--text-data-lg-weight);
  line-height: var(--text-data-lg-line);
  font-variant-numeric: tabular-nums;
}
.measure { max-width: var(--measure); }
.muted { color: var(--color-ink-muted); }
.prose > * + * { margin-block-start: var(--space-4); }
.prose { max-width: var(--measure); }
.numeric { text-align: right; font-variant-numeric: tabular-nums; }

/* ── Accessibility primitives ─────────────────────────────────────────────────────────────── */

/* Out of flow: `position: absolute`, so it sits wherever its containing block puts it, not where
   its parent's box ends. Any scroll container that can hold one must be positioned itself, or the
   label escapes the scroll box and stretches the page it sits on. That is why the data table's
   wrapper is `position: relative`: a header's hidden label did exactly this until the Story 3.1
   staging proof found it. */
.visually-hidden {
  position: absolute;
  width: var(--space-1);
  height: var(--space-1);
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* --focus-ring-width at --focus-ring-offset on everything focusable, never removed - not for aesthetics, not on mouse
   input. The ring is chosen by the luminance it lands on: light surfaces take --color-focus-ring
   (dark under the staff query), a dark band inside a light surface takes the on-ink ring. */
:focus-visible {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}
.on-ink :focus-visible,
.on-ink:focus-visible { outline-color: var(--color-focus-ring-on-ink); }
.on-ink .user-menu-panel :focus-visible { outline-color: var(--color-focus-ring); }
.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-4);
  border-radius: var(--rounded-md);
  background: var(--color-ink);
  color: var(--color-ink-inverse);
  font-weight: var(--text-action-weight);
  text-decoration: none;
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }
.glyph { flex: none; width: 1em; height: 1em; }

/* ── Shells ───────────────────────────────────────────────────────────────────────────────── */

/* Staff top bar (and the plain chain's header): chrome on the raised layer, a hairline below.
   Below the `md` breakpoint the raised layer is the dark one, through the tokens alone. */
.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  min-height: var(--touch-critical);
  padding: var(--space-2) var(--gutter-desktop);
  background: var(--color-surface-raised);
  border-bottom: var(--border-hairline) solid var(--color-border);
}
.wordmark {
  font-size: var(--text-title-size);
  font-weight: var(--text-display-weight);
  font-style: italic;
  line-height: var(--text-title-line);
  letter-spacing: var(--text-display-tracking);
  white-space: nowrap;
}
/* Red appears here, in an alarm state and on a destructive affordance. Nowhere else. */
.wordmark-ros { color: var(--color-brand); }
.on-ink .wordmark-ros { color: var(--color-brand-on-ink); }
.nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-3);
  border-radius: var(--rounded-md);
  font-weight: var(--text-heading-weight);
  text-decoration: none;
}
/* The current section is underlined, never coloured: red is not a nav highlight. */
.nav a[aria-current="page"] {
  text-decoration: underline;
  text-decoration-thickness: var(--border-strong);
  text-underline-offset: var(--space-1);
}
/* The identity is a control: one tap reaches sign-out. The auto margin is its own, because the
   plain chain's header (base.html) holds the identity with no group around it. */
.identity { margin-inline-start: auto; }
/* On the staff bar it shares the end with Mi perfil, and only with it: the pair is pushed to the
   end together so the identity keeps the corner a thumb reaches for. It wraps like the bar it sits
   in - the Almacén at phone width has to be able to put the two controls on their own line rather
   than squeeze them off it. */
.identity-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-inline-start: auto;
}
.identity-button {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  min-height: var(--touch-min);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--rounded-md);
  line-height: var(--text-action-line);
  text-align: right;
  overflow-wrap: anywhere;
}
.identity-email { font-weight: var(--text-heading-weight); }
.identity-action { color: var(--color-ink-muted); }
/* The rendered back affordance every inner staff page carries: --touch-min, a glyph, a word. */
.back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding: 0 var(--space-2);
  border-radius: var(--rounded-md);
  font-weight: var(--text-heading-weight);
  text-decoration: none;
}
/* Portal app bar: the ink band inside the light surface. Everything in it is on-ink. */
.appbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  min-height: var(--touch-critical);
  padding: var(--space-2) var(--gutter-desktop);
  background: var(--color-ink);
  color: var(--color-ink-inverse);
}
.user-menu { position: relative; margin-inline-start: auto; }
.user-menu > summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-3);
  border-radius: var(--rounded-md);
  list-style: none;
  font-weight: var(--text-heading-weight);
  overflow-wrap: anywhere;
}
.user-menu > summary::-webkit-details-marker { display: none; }
/* A strong hairline, not a shadow: only the consequence dialog casts one. */
.user-menu-panel {
  display: grid;
  /* Two controls now, stacked: the gap is what keeps adjacent targets apart in the disclosure. */
  gap: var(--space-2);
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 1;
  min-width: 100%;
  margin-top: var(--space-1);
  padding: var(--space-2);
  background: var(--color-surface-base);
  color: var(--color-ink);
  border: var(--border-hairline) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
}
.main {
  flex: 1;
  width: 100%;
  padding: var(--space-6) var(--gutter-desktop);
}
.main-plain { max-width: var(--measure); margin: 0 auto; }
/* Portal content is capped and centred from `lg` up; below it reflows (WCAG 1.4.10). */
.main-portal { max-width: var(--breakpoint-lg); margin: 0 auto; }
.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-4) var(--gutter-desktop);
  border-top: var(--border-hairline) solid var(--color-border);
  color: var(--color-ink-muted);
}
.footer-link { display: inline-flex; align-items: center; min-height: var(--touch-min); }
/* The connectivity line: persistent and non-alarming, muted ink with a glyph, never red. */
.connectivity {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--gutter-desktop);
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
  border-bottom: var(--border-hairline) solid var(--color-border);
}
.connectivity[hidden] { display: none; }
/* The live region: outside every swap target (base.html), filled by an out-of-band swap.
   It is a status, not a toast - it stays until the next status replaces it. */
.status {
  position: fixed;
  bottom: var(--space-4);
  left: 50%;
  z-index: 1;
  max-width: calc(100% - var(--gutter-mobile) * 2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--rounded-md);
  background: var(--color-ink);
  color: var(--color-ink-inverse);
  transform: translateX(-50%);
}
/* Never display:none: a live region taken out of the accessibility tree and put back in the
   same tick as its first update may not be announced at all. Empty, it keeps its box but
   paints and catches nothing. */
.status:empty { padding: 0; background: none; pointer-events: none; }
.footer-note {
  margin: 0;
  font-size: var(--text-body-sm-size);
  line-height: var(--text-body-sm-line);
  color: var(--color-ink-muted);
}
.page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-block-end: var(--space-6);
}
.page-intro {
  max-width: var(--measure);
  margin-block-end: var(--space-6);
  color: var(--color-ink-muted);
}

/* ── Buttons (DESIGN.md components.button-*) ──────────────────────────────────────────────── */

.button-primary, .button-secondary, .button-danger, .button-danger-solid {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-4);
  border: var(--border-interactive) solid transparent;
  border-radius: var(--rounded-md);
  background: transparent;
  color: inherit;
  font-size: var(--text-action-size);
  font-weight: var(--text-action-weight);
  line-height: var(--text-action-line);
  text-decoration: none;
  cursor: pointer;
}
/* The one filled button on a normal screen: near-black, never red. */
.button-primary {
  background: var(--color-ink);
  color: var(--color-ink-inverse);
  border-color: var(--color-ink);
}
.button-secondary { color: var(--color-ink); border-color: var(--color-border-strong); }
/* Outlined in red with red text - the affordance of a destructive action, not a commit. */
.button-danger {
  background: var(--color-button-danger-background);
  color: var(--color-danger);
  border-color: var(--color-danger);
}
/* The only red fill in the product: the commit control inside a consequence dialog. */
.button-danger-solid {
  background: var(--color-danger-strong);
  color: var(--color-ink-inverse);
  border-color: var(--color-danger-strong);
}
.button-primary:disabled, .button-secondary:disabled,
.button-danger:disabled, .button-danger-solid:disabled { cursor: progress; opacity: .6; }
/* Critical operator actions: --touch-critical, sized for a glance at arm's length. */
.critical {
  min-height: var(--touch-critical);
  font-size: var(--text-action-operator-size);
  font-weight: var(--text-action-operator-weight);
  line-height: var(--text-action-operator-line);
}
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--touch-gap);
}

/* ── Forms (DESIGN.md components.form-field; rendered by common/templates/django/forms/) ──── */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-width: var(--measure);
  margin-block-end: var(--space-4);
}
/* Not uppercased, unlike .label: a field label carries units, and text-transform would render
   m³ as M³ - M is mega, and the volume every euro of Storage revenue derives from is not one
   that may be miscased by a stylesheet. */
.field-label {
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
  color: var(--color-ink-muted);
}
/* --touch-min, the strong border - the only thing marking where the control is (WCAG 1.4.11). */
.field input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
.field select,
.field textarea {
  display: block;
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  background: var(--color-field-background);
  color: var(--color-ink);
  border: var(--border-interactive) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
}
.field textarea { min-height: calc(var(--touch-min) * 2); }
.field input[type="checkbox"], .field input[type="radio"] {
  width: var(--space-6);
  height: var(--space-6);
  margin: 0;
  accent-color: var(--color-ink);
}
/* A checkbox or radio group: what `field.use_fieldset` renders around a MultipleChoiceField
   (accounts.forms.StaffUserForm's Permisos). One labelled group - <fieldset> plus <legend> - with
   the browser's own box, padding and min-inline-size dropped, so it sits in the .field column like
   every other control and reflows on a narrow screen instead of refusing to shrink past its
   widest option (a fieldset's own min-inline-size is min-content). */
.fieldset {
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
/* The option list and the help line, each separated from what precedes it the way .field is. */
.fieldset > div { margin-block-start: var(--space-1); }
/* One option per row, the whole row - box and text alike - a --touch-min target. */
.fieldset label {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-min);
}
.field-invalid input, .field-invalid select, .field-invalid textarea {
  border: var(--border-strong) solid var(--color-danger);
}
.field-help { color: var(--color-ink-muted); }
.field-help ul { margin: 0; padding-inline-start: var(--space-5); }
/* The error line: glyph and text, the text names the fix. Never colour alone. */
.field-errors, .form-errors {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--color-danger);
}
.field-errors li, .form-errors li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.form-errors {
  max-width: var(--measure);
  margin-block-end: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: var(--border-hairline) solid var(--color-danger);
  border-radius: var(--rounded-md);
}
.form-aside { margin-block-start: var(--space-6); }
/* The parsed-value preview under a numeric input, in the data face. */
.decimal-echo {
  display: block;
  margin-block-start: var(--space-1);
  color: var(--color-ink-muted);
}
.decimal-echo:empty { display: none; }
form[aria-busy="true"] { cursor: progress; }

/* A unit symbol is never uppercased, wherever it is rendered: `m³` is metres cubed and `M³` is
   mega-cubed, and the volume every euro of Storage revenue derives from is not one a stylesheet
   may miscase. It overrides `.label` - the one uppercase style in the product - and `.field-label`
   already avoids the problem the other way, by not being uppercase at all. */
.unit { text-transform: none; }
/* The symbol beside a numeric input (common/templates/django/forms/widgets/decimal.html). Muted
   and on its own line under the field, so it reads as the field's unit and never as its value. */
.field-unit {
  display: block;
  margin-block-start: var(--space-1);
  color: var(--color-ink-muted);
}

/* ── The reception screen: the Client picker and the Line editor (Story 2.1) ──────────────── */

/* The picker is an ordinary <select> - the search box above it narrows the options and the two
   <optgroup>s put the Clients this Operator last received for at the top. Nothing here is a
   control of our own: a type-ahead built out of divs stops working the moment JavaScript does,
   and what the server accepts is the field's queryset either way. */
.reception-clients { margin-block-end: var(--space-4); }
.reception-clients optgroup { font-weight: var(--text-heading-weight); }

/* One card per article line, sunken so the repeating block reads as a group rather than as more
   form. The controls sit under the fields and keep the end of the row. */
.lines { margin-block: var(--space-6); }
.line-list { margin: 0; padding: 0; list-style: none; }
.line-item {
  margin-block-end: var(--space-4);
  padding: var(--space-4);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-md);
  background: var(--color-surface-sunken);
}
.line-fields { display: flex; flex-wrap: wrap; gap: var(--touch-gap); }
/* The three fields share a row: the description takes the room a real article description needs
   and the other two take what a number needs. `min-width: 0` so a long description wraps instead
   of pushing the quantity and its unit off the row. Below the breakpoint they stack, in the one
   media query this stylesheet has. */
.line-fields > .field {
  flex: 1 1 20%;
  min-width: 0;
  max-width: none;
  margin-block-end: 0;
}
.line-fields > .field:first-child { flex: 3 1 40%; }
.line-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--touch-gap);
  margin-block-start: var(--space-3);
}
.line-controls > * { min-height: var(--touch-min); }

/* ── Photos of the delivery note (DESIGN.md components.photo-capture, Story 2.2) ──────────── */

/* The capture island, carried by the reception form and by the note's own photos screen. Sunken
   like a Line card, so the repeating block reads as a group rather than as more form. */
.photo-capture {
  margin-block: var(--space-6);
  padding: var(--space-4);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-md);
  background: var(--color-surface-sunken);
}
.photo-capture-heading {
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
  color: var(--color-ink-muted);
  text-transform: uppercase;
}
.photo-capture-actions { margin-block-start: var(--space-4); }
/* The island's own refusal - a 13th page, an oversized file, something that is not an image. Not
   red: it is the same words the server would say and it is about the file, not about the person. */
.photo-refusal {
  margin-block-start: var(--space-3);
  color: var(--color-ink);
}
.photo-count {
  margin-block-start: var(--space-2);
  font-size: var(--text-body-sm-size);
  line-height: var(--text-body-sm-line);
  color: var(--color-ink-muted);
}
/* Why the images went, shown with the control that asks for new ones. Not red: a presigned URL
   expiring is the designed state six years into the Historial, not a failure anybody caused. */
.photo-expired {
  margin-block-start: var(--space-3);
  color: var(--color-ink-muted);
}
.photo-refusal[hidden], .photo-count[hidden], .photo-inputs[hidden],
.photo-expired[hidden] { display: none; }
/* DESIGN.md components.photo-capture.thumbnailSize is --thumbnail-size, and the remove control
   goes under each thumbnail rather than beside the capture button - a destructive control is never
   adjacent to a critical one (DESIGN.md > Touch Targets), and --space-6 is the separation it asks
   for. */
.photo-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin: 0;
  margin-block-start: var(--space-6);
  padding: 0;
  list-style: none;
}
.photo-thumb {
  display: flex;
  flex-direction: column;
  gap: var(--touch-gap);
  align-items: center;
}
.photo-thumb-image {
  width: var(--thumbnail-size);
  height: var(--thumbnail-size);
  object-fit: cover;
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-sm);
  background: var(--color-surface-raised);
}
.photo-thumb .button-danger { min-height: var(--touch-min); }

/* The gallery: what the note actually holds, server-rendered from short-lived presigned GETs and
   therefore always outside the preserved island. View-only in place - no download, share or print
   affordance anywhere (EXPERIENCE > Privacy). */
.photo-gallery { margin-block: var(--space-6); }
.photo-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.photo-item { margin: 0; }
/* Bigger than a thumbnail on purpose - this is the page being read back against the paper - and
   capped by the reading measure so a desktop does not print one photograph across a monitor. */
.photo-image {
  max-width: 100%;
  width: calc(var(--thumbnail-size) * 4);
  height: auto;
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-sm);
  background: var(--color-surface-raised);
}
.photo-gallery-actions { margin-block-start: var(--space-4); }
.photo-gallery-actions [hidden] { display: none; }

/* ── State pill (DESIGN.md components.state-pill) ─────────────────────────────────────────── */

/* Always a solid fill with inverse text, a glyph and a label; sizes to content and wraps to a
   second line rather than truncating - no layout may assume a pill width. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--rounded-full);
  color: var(--color-ink-inverse);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
  text-transform: uppercase;
}
.pill-idle { background: var(--color-idle); }
.pill-info { background: var(--color-info); }
.pill-warning { background: var(--color-warning); }
.pill-success { background: var(--color-success); }
.pill-danger { background: var(--color-danger); }

/* ── Tiles and the count badge (DESIGN.md components.action-tile) ─────────────────────────── */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(calc(var(--touch-critical) * 4), 1fr));
  gap: var(--touch-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.tile-item { display: flex; }
/* The whole tile is the target. */
.tile {
  position: relative;
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: flex-end;
  min-height: var(--tile-min-height);
  padding: var(--space-4);
  background: var(--color-surface-raised);
  color: var(--color-ink);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-lg);
  font-size: var(--text-action-operator-size);
  font-weight: var(--text-action-operator-weight);
  line-height: var(--text-action-operator-line);
  text-decoration: none;
}
.badge {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--space-6);
  min-height: var(--space-6);
  padding: 0 var(--space-2);
  border-radius: var(--rounded-full);
  background: var(--color-warning);
  color: var(--color-ink-inverse);
  font-family: var(--font-mono);
  font-size: var(--text-data-size);
  font-weight: var(--text-data-lg-weight);
  line-height: var(--text-data-line);
  font-variant-numeric: tabular-nums;
}

/* ── The delivery-note card (DESIGN.md components.delivery-note-card) ─────────────────────── */

/* The Operator's primary object, and the first list in the product laid out as cards rather than
   as a table. One column: the put-away queue is read on a phone at a lorry door, and a grid would
   put two critical targets side by side under one gloved thumb. */
.note-card-list {
  display: flex;
  flex-direction: column;
  gap: var(--touch-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.note-card-item { display: flex; }
/* The whole card is the target (DESIGN.md > Touch Targets: where the target is a row, the row is
   the target). `--color-surface-raised` resolves to `{colors.surface-sunken}` on the light surface
   and `{colors.surface-raised-dark}` on the dark one, which is exactly the card's two-mode
   mapping. `border`, not `border-strong`: the card's edge is a hairline between rows, and what
   marks it as interactive is that the whole card is the link, at --touch-critical on the phone,
   with a data-lg identifier in it. */
.note-card {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  background: var(--color-surface-raised);
  color: var(--color-ink);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-md);
  text-decoration: none;
}
/* The physical Delivery-Note Number leads. `overflow-wrap: anywhere` is the load-bearing line: a
   Split child's number is the parent's plus a suffix, and eliding the trailing characters would
   make two siblings' cards read identically (DESIGN.md:453-455). It wraps; it never truncates. */
.note-card-number {
  font-family: var(--font-mono);
  font-size: var(--text-data-lg-size);
  font-weight: var(--text-data-lg-weight);
  line-height: var(--text-data-lg-line);
  overflow-wrap: anywhere;
}
/* The internal code beneath it, and - on a staff card whose caller passes one - the Ubicación
   beneath that (Story 2.6, EXPERIENCE.md:212). One set of `{components.delivery-note-card}` tokens
   for both: they are the two machine identifiers the card carries after the paper's own number. The
   Ubicación wraps rather than truncating for the number's reason - "Estantería C, nivel 3" and
   "Estantería C, nivel 31" differ only at the end. */
.note-card-code,
.note-card-location {
  color: var(--color-ink-muted);
  font-family: var(--font-mono);
  font-size: var(--text-data-size);
  font-weight: var(--text-data-weight);
  line-height: var(--text-data-line);
}
.note-card-location { overflow-wrap: anywhere; }
/* *Últimos ubicados*, the Ubicar screen's second block (Story 2.6): a <section> named by its
   heading. It sits a full --space-8 below whatever ends the queue - a list or its empty state - so
   the two blocks read as two, and the caption naming the bound hangs directly under the heading. */
.note-card-section { margin-block-start: var(--space-8); }
.note-card-caption {
  max-width: var(--measure);
  margin-block: var(--space-1) var(--space-4);
  color: var(--color-ink-muted);
}
/* Pending Relocation: a **badge**, never a pill (DESIGN.md:443-449). The same amber-and-inverse
   pairing the tile's count badge uses, laid out inline because this one carries words - and
   `align-self` so it takes its text's width rather than the card's. */
.note-card-flag {
  align-self: flex-start;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--rounded-sm);
  background: var(--color-warning);
  color: var(--color-ink-inverse);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
}

/* The failed-delivery re-admit's *nota para quien ubique* (Story 4.8), right after the badge: free
   text an Operator typed at the van, so it wraps anywhere and is never elided. */
.note-card-relocation {
  overflow-wrap: anywhere;
  white-space: pre-line;
}
.putaway-flag {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
}

/* Relocation's "Ahora está en …" (Story 2.6): where the goods are before anything moves, above the
   picker that `aria-describedby` ties it to. Body text at the reading measure; not muted, because
   it is the one fact on the screen the Operator must not misread. */
.putaway-current {
  max-width: var(--measure);
  margin-block-end: var(--space-4);
}

/* ── Finding a note, and reading it (Story 2.7, CAP-16) ───────────────────────────────────── */

/* {components.search-field} (DESIGN.md:218-227, :457): the Panel's first control and the results
   page's own, one template. The field itself is --touch-critical on every surface - DESIGN.md names
   the search field in the Critical class - with the form-field boundary (interactive border,
   strong colour) because it is the only thing marking where a borderless-looking box is. The button
   beside it wraps under it rather than squeezing it on the narrowest phone. */
.search-field {
  display: flex;
  flex-wrap: wrap;
  gap: var(--touch-gap);
  max-width: var(--measure);
  margin-block: var(--space-3) var(--space-6);
}
.search-field-input {
  flex: 1 1 calc(var(--touch-critical) * 4);
  min-width: 0;
  min-height: var(--touch-critical);
  padding: var(--space-2) var(--space-4);
  background: var(--color-field-background);
  color: var(--color-ink);
  border: var(--border-interactive) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
  font-size: var(--text-body-size);
  line-height: var(--text-body-line);
}
.search-field-input::placeholder { color: var(--color-ink-muted); }
.search-field .critical { flex: 0 0 auto; }
/* The Portal's search (Story 3.1) keeps its label visible above the field, which the staff one
   hides. The label-over-control column is `.field`'s, restated here rather than reused: inside
   `.field`, the input rule (0,4,1) outranks `.search-field-input` (0,1,0), and the field computed
   only --touch-min on a phone, where *Buscar* wraps under it and no flex line stretches it. */
.portal-search {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-width: var(--measure);
  margin-block-end: var(--space-4);
}

/* The state chips: links, one per state plus *Todos*, --touch-min each and --touch-gap apart. The
   current one is filled and the rest outlined - a shape difference as well as a colour one, and
   `aria-current` says it to a screen reader. Never red, never a state tone: a chip is a filter, and
   colouring it like the pill would make it read as a state the page is in. */
.chips { margin-block-end: var(--space-6); }
.chip-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--touch-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  padding: 0 var(--space-4);
  border: var(--border-interactive) solid var(--color-border-strong);
  border-radius: var(--rounded-full);
  color: var(--color-ink);
  font-weight: var(--text-action-weight);
  text-decoration: none;
}
.chip[aria-current="true"] {
  background: var(--color-ink);
  color: var(--color-ink-inverse);
  border-color: var(--color-ink);
}

/* The empty branches: the sentence, then at most two ways forward in the shared action row. */
.search-empty .actions { margin-block-end: var(--space-6); }

/* Pagination (components/pagination.html): *Anterior*, the position, *Siguiente*. Both links are
   --touch-critical on every surface - a strip of small numbered targets is the fine-gesture control
   this product bans - and outlined like a secondary button, because turning a page is not the
   screen's primary action. */
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--touch-gap);
  margin-block-start: var(--space-6);
}
.pagination-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-critical);
  padding: 0 var(--space-4);
  border: var(--border-interactive) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
  color: var(--color-ink);
  font-size: var(--text-action-operator-size);
  font-weight: var(--text-action-operator-weight);
  line-height: var(--text-action-operator-line);
  text-decoration: none;
}
.pagination-position { color: var(--color-ink-muted); }

/* A search result's two extra lines (the card's own rules are above). The pill takes its own width
   rather than the card's, and the Client's name wraps like every other identifier on the card. */
.note-card-state { align-self: flex-start; }
.note-card-client { overflow-wrap: anywhere; }

/* *Detalle de albarán*. The heading leads with the paper's number, in the identifier face, wrapping
   and never elided (DESIGN.md:453-455); the code under it, then the pill and the badge. */
.note-detail-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-block-end: var(--space-6);
}
.note-detail-number {
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
}
.note-detail-code {
  color: var(--color-ink-muted);
  font-family: var(--font-mono);
  font-size: var(--text-data-size);
  line-height: var(--text-data-line);
}
.note-detail-status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-block-start: var(--space-2);
}
.note-detail-actions { margin-block-end: var(--space-6); }
/* The facts as a definition list: label and value side by side from the `md` breakpoint up, one
   under the other on the phone (the query at the foot). A hairline between rows, never a box: this
   is reading, not a form. */
.note-facts {
  display: grid;
  gap: var(--space-3);
  max-width: var(--measure);
  margin-block-end: var(--space-8);
}
.note-fact {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: var(--space-1) var(--space-4);
  padding-block-end: var(--space-3);
  border-block-end: var(--border-hairline) solid var(--color-border);
}
.note-fact dt {
  color: var(--color-ink-muted);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
}
.note-fact dd { margin: 0; overflow-wrap: anywhere; }
/* *desde … (hace …)* sits on its own line under the value, muted: the value is what was asked. */
.note-fact-since { display: block; color: var(--color-ink-muted); }
.note-detail-section { max-width: var(--measure); margin-block-end: var(--space-8); }
.note-detail-section > h2 { margin-block-end: var(--space-3); }
.note-lines { margin: 0; padding-inline-start: var(--space-6); }
.note-line {
  padding-block: var(--space-2);
  border-block-end: var(--border-hairline) solid var(--color-border);
  overflow-wrap: anywhere;
}
.note-line-quantity { display: block; color: var(--color-ink-muted); }
/* The *Historial* as a timeline: a rule down the start edge and one entry per step, newest first.
   The time is an identifier (the data face), the action is what a reader scans for, the actor and
   the changes are what they read next. */
.history {
  margin: 0;
  padding: 0;
  list-style: none;
  border-inline-start: var(--border-strong) solid var(--color-border);
}
.history-entry { padding: 0 0 var(--space-4) var(--space-4); }
.history-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
}
.history-when {
  color: var(--color-ink-muted);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.history-action { font-weight: var(--text-heading-weight); }
.history-actor { color: var(--color-ink-muted); overflow-wrap: anywhere; }
.history-changes {
  margin: 0;
  margin-block-start: var(--space-1);
  padding-inline-start: var(--space-5);
}
.history-changes li { overflow-wrap: anywhere; }
.history-field { font-weight: var(--text-heading-weight); }

/* ── Sin terminar: the local-draft list (Story 1.11) ──────────────────────────────────────── */

/* Above the tiles on the staff home and above the Portal home's empty state, and filled by
   drafts.js out of this browser's IndexedDB - so the section ships empty and `hidden`, and stays
   that way with JavaScript off, with no drafts, or with a store that will not open. A sunken
   band rather than a raised card: it is a note about work already in progress, not a tile
   offering new work. */
.unfinished {
  margin-block-end: var(--space-6);
  padding: var(--space-4);
  background: var(--color-surface-sunken);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-lg);
}
.unfinished[hidden] { display: none; }
.unfinished-intro {
  max-width: var(--measure);
  margin-block: var(--space-1) var(--space-4);
  color: var(--color-ink-muted);
}
.unfinished-list {
  display: flex;
  flex-direction: column;
  gap: var(--touch-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}
.unfinished-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--touch-gap);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-base);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-md);
}
/* min-width: 0 so a long form name wraps instead of pushing the two controls off the row. */
.unfinished-text { flex: 1 1 auto; min-width: 0; }
.unfinished-label { font-weight: var(--text-action-weight); }
.unfinished-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  color: var(--color-ink-muted);
  font-size: var(--text-body-sm-size);
  line-height: var(--text-body-sm-line);
}
/* A timestamp is an identifier, not prose (DESIGN.md typography.data). */
.unfinished-when { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
/* The row reuses .actions, so both controls are --touch-min from .button-* and --touch-gap
   apart - Descartar is destructive and must never sit flush against Continuar. */
.unfinished-actions { flex: 0 0 auto; }

/* ── Fragment states (EXPERIENCE > State Patterns) ────────────────────────────────────────── */

/* Loading: in place, sized to a target so the swap shifts nothing. htmx's own indicator styles
   are switched off in base.html's htmx-config; these are the equivalent. */
.loading {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  color: var(--color-ink-muted);
}
.spinner {
  width: 1em;
  height: 1em;
  border: var(--border-strong) solid var(--color-border-strong);
  border-top-color: var(--color-ink);
  border-radius: var(--rounded-full);
  animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { display: flex; }
.empty {
  max-width: var(--measure);
  padding-block: var(--space-6);
  color: var(--color-ink-muted);
}
/* A request that failed for a reason the user did not cause: plain words, input kept, retry
   on the thing that failed. Not red - it is neither validation nor a refusal. */
.fragment-error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-block-end: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-sunken);
  border: var(--border-hairline) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
}
.fragment-error-text { flex: 1 1 auto; }
/* Every wide table scrolls inside its own container; the page body never scrolls sideways.
   overflow:hidden here is a defect (DESIGN.md data-table.containerOverflowX).
   `position: relative` makes the wrapper the containing block of everything it holds, so nothing
   inside can escape the scroll box. A header's `.visually-hidden` label is `position: absolute`;
   while the wrapper was static, that label's containing block sat outside it, and the label
   stretched the page itself, past the viewport, on a phone and at 200% zoom (the Story 3.1
   staging proof: a document 736 wide in a 320 viewport on the Portal's list, 472 on *Mi
   equipo*). */
.table-wrap { position: relative; max-width: 100%; overflow-x: auto; }

/* ── Data table (DESIGN.md components.data-table) ─────────────────────────────────────────── */

/* Always inside .table-wrap, never on its own: the container is what scrolls. */
.data-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}
.data-table th, .data-table td {
  padding: var(--space-3) var(--space-4);
  /* A hairline between rows - decorative, never a component's only boundary. */
  border-block-end: var(--border-hairline) solid var(--color-border);
  vertical-align: middle;
}
/* The sunken header band, in the one uppercase style in the product. */
.data-table thead th {
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
  text-transform: uppercase;
  white-space: nowrap;
}
.data-table tbody th { font-weight: var(--text-heading-weight); }
/* Identifiers, m³, money and timestamps in the data face; numbers right so they stack. */
.data-table .data { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.data-table .numeric {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.data-table tbody tr[aria-selected="true"] { background: var(--color-surface-selected); }
/* The same tint for a row whose own checkbox is checked (Story 4.1's builder), drawn from the
   control itself so the box and the tint can never disagree - and never the tint alone. */
.data-table tbody tr:has(input:checked) { background: var(--color-surface-selected); }

/* *Trazabilidad* (Story 7.9): the global history as a data table. A row grows when its disclosure
   opens, so every cell starts at the top where the eye reads the time. The time is an identifier
   and is kept whole. The disclosure's control is a --touch-min target; the recorded payloads keep
   their indentation and wrap inside their cell - a long value never widens the table. */
.trace-table th, .trace-table td { vertical-align: top; }
.trace-table td.data { white-space: nowrap; }
.trace-recorded > summary {
  box-sizing: border-box;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding-block: var(--space-3);
}
.trace-recorded > p { margin-block-start: var(--space-2); }
.trace-recorded > pre {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* *Mis albaranes en almacén* (Story 3.1): the Client's stock as a data table. A row can be several
   Lines tall, so every cell starts at the top where the eye reads the number. The number leads in
   the identifier face and wraps rather than truncating - a Split child's suffix is the whole
   difference between siblings - with the code beneath it, muted, the card's two lines in one cell.
   The Lines are a compact list with the quantity beside each description; the cell keeps enough
   width for a description to read as words, and past that the table scrolls in `.table-wrap`. */
.stock-table th, .stock-table td { vertical-align: top; }
/* The identifier cell is one target: the link to the *Detalle de albarán* holds the number and
   the code beneath it, so both lines are what a finger lands on, at least --touch-min each way
   (DESIGN.md > Touch Targets sets Standard for every interactive element on the Portal; the number
   alone, as bare text, was one line tall). A block rather than a centred flex box, so the number's
   first line stays level with every other cell's and the row grows by nothing. The number keeps
   its underline; the code under it stays muted and plain, as it was. */
.stock-link {
  display: block;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  text-decoration: none;
}
.stock-number {
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
  text-decoration: underline;
}
.stock-code {
  display: block;
  color: var(--color-ink-muted);
  font-family: var(--font-mono);
  font-size: var(--text-data-size);
  font-weight: var(--text-data-weight);
  line-height: var(--text-data-line);
}
.stock-lines {
  min-width: calc(var(--touch-critical) * 4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.stock-line + .stock-line { margin-block-start: var(--space-1); }
.stock-line-quantity { color: var(--color-ink-muted); white-space: nowrap; }
/* The Portal detail's way back to that list, a --touch-min target like the row link that led
   there (as bare text it was one line tall). Its own class, because the `.form-aside` around it is
   shared with staff screens and the error pages. */
.note-detail-back {
  display: inline-flex;
  align-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
}

/* ── Nueva petición de entrega and Mis peticiones (Story 4.1) ─────────────────────────────── */

/* A choice that is its own target. DESIGN.md > Touch Targets puts every interactive element on the
   Portal at --touch-min, and a checkbox or a radio is one: so the input **itself** is the target.
   The `.choice` box is --touch-min each way, the input covers it, transparent - still focusable,
   still announced, still the thing a finger lands on - and the mark is drawn on the box at the
   field's own size. As specific as `.field input[type="radio"]` and declared after it, so a radio
   inside a field takes this box rather than that rule's smaller one. */
.choice {
  position: relative;
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  vertical-align: middle;
}
.choice > input[type="checkbox"],
.choice > input[type="radio"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
/* The mark: the form-field boundary - interactive width, strong colour, the only thing marking
   where the control is (WCAG 1.4.11). Presses go through it to the input. */
.choice::before {
  content: "";
  width: var(--space-6);
  height: var(--space-6);
  border: var(--border-interactive) solid var(--color-border-strong);
  border-radius: var(--rounded-sm);
  background: var(--color-field-background);
  pointer-events: none;
}
.choice-radio::before { border-radius: var(--rounded-full); }
/* Checked: filled with ink and marked in the inverse colour - a shape as well as a fill. */
.choice:has(> input:checked)::before {
  background: var(--color-ink);
  border-color: var(--color-ink);
}
.choice:has(> input:checked)::after {
  content: "";
  position: absolute;
  pointer-events: none;
}
.choice-checkbox:has(> input:checked)::after {
  width: var(--space-2);
  height: var(--space-3);
  border: solid var(--color-ink-inverse);
  border-width: 0 var(--border-strong) var(--border-strong) 0;
  transform: translateY(-15%) rotate(45deg);
}
.choice-radio:has(> input:checked)::after {
  width: var(--space-2);
  height: var(--space-2);
  border-radius: var(--rounded-full);
  background: var(--color-ink-inverse);
}
/* The input is transparent, so its own ring would be too: the product's one ring is drawn on the
   mark instead, from the same tokens as `:focus-visible`. Never removed. */
.choice:has(> input:focus-visible)::before {
  outline: var(--focus-ring-width) solid var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
}
/* An invalid group's boxes take the invalid field's boundary, as every other control does. */
.field-invalid .choice::before { border: var(--border-strong) solid var(--color-danger); }
/* Two places the drawn mark cannot be trusted, and the native control comes back instead, still
   the size of the box: a browser without `:has()`, which could draw neither the checked state nor
   the ring on the mark, and forced colours (Windows High Contrast), which would drop a mark drawn
   as a background. The UA draws a native control in the user's own colours, ring included. */
@supports not selector(:has(*)) {
  .choice > input[type="checkbox"],
  .choice > input[type="radio"] { opacity: 1; }
  .choice::before,
  .choice::after { content: none; }
}
@media (forced-colors: active) {
  .choice > input[type="checkbox"],
  .choice > input[type="radio"] { opacity: 1; }
  .choice::before,
  .choice::after,
  .choice:has(> input:checked)::after { content: none; }
}
/* The Shift's options in a row, wrapping on a narrow screen, --touch-gap apart. Each option is a
   `.fieldset label`, so box and word together are one --touch-min row. */
.choice-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--touch-gap);
}

/* The builder's two parts, a heading each. */
.request-notes, .request-when { margin-block-end: var(--space-6); }
.request-when > h2 { margin-block-end: var(--space-3); }
/* The Request Type (Story 4.9, J3): choosing *Recogida en almacén* hides the address and reads the
   date as *Fecha de recogida*, with **no JavaScript** - the checked radio is the state, so it holds
   on a 422 swap and after a draft restore alike (`tr:has(input:checked)` is the precedent). A
   browser without `:has()` shows the address and *Fecha deseada*: the form still submits, and the
   service drops a Pickup's address. */
.request-date-pickup { display: none; }
form:has(input[name="request_type"][value="pickup"]:checked) .request-address,
form:has(input[name="request_type"][value="pickup"]:checked) .request-date-delivery {
  display: none;
}
form:has(input[name="request_type"][value="pickup"]:checked) .request-date-pickup {
  display: inline;
}
/* A Pickup's badge and slot (`delivery_requests/_slot.html`): words in an outline - never a pill
   and never a state tone, because a type is not a state - then the day and the Shift. */
.request-type-badge {
  display: inline-block;
  padding: 0 var(--space-2);
  border: var(--border-hairline) solid var(--color-border-strong);
  border-radius: var(--rounded-sm);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
}
/* The list's filter, said in words under its heading (EXPERIENCE > State Patterns, *Reserved by
   another Petición*): what is absent from the table is absent on purpose. */
.request-filter {
  max-width: var(--measure);
  margin-block: var(--space-1) var(--space-4);
  color: var(--color-ink-muted);
}
/* The whole row is the target (DESIGN.md > Touch Targets): every cell is a `<label>` for the row's
   checkbox, and its pseudo-element is stretched over the cell, padding included, so a press
   anywhere in it lands on the label. The cells are positioned for that, and whatever is absolute
   inside one stays inside the scroll box. */
.request-table tbody td,
.request-table tbody th { position: relative; }
.request-cell { cursor: pointer; }
.request-cell::before {
  content: "";
  position: absolute;
  inset: 0;
}
/* The number leads in the identifier face and wraps rather than truncating: a Split child's suffix
   is the whole difference between siblings. */
.request-number {
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
}
/* The code and the date are fixed-width identifiers and are kept whole: `RL-` on one line and its
   digits on the next is two half-identifiers. The table scrolls in its wrap instead. */
.request-table .data { white-space: nowrap; }
/* A row the submission lost, marked where its checkbox was: the error line's glyph, words and
   colour, because it is half of the refusal above the table. */
.request-lost-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--color-danger);
  font-weight: var(--text-heading-weight);
  white-space: nowrap;
}
/* What sending does, under the two actions it describes. */
.request-hint {
  max-width: var(--measure);
  margin-block-start: var(--space-2);
  color: var(--color-ink-muted);
}
/* *Mis peticiones*: the address keeps room to read as an address, and wraps past it; beyond that
   the table scrolls in its wrap. */
.requests-address {
  min-width: calc(var(--touch-critical) * 4);
  overflow-wrap: anywhere;
}

/* ── Peticiones (Story 4.2) ───────────────────────────────────────────────────────────────── */

/* The code that opens a Request - or, on its page, a note - is a --touch-min target (DESIGN.md >
   Touch Targets), never a bare line of text, and kept whole: half a code is no code. */
.requests-queue-link {
  display: inline-flex;
  align-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  white-space: nowrap;
}
/* The delivered note's link to its proof (Story 4.7) is a sentence, not a code: a --touch-min
   target like the codes, but in the body face and wrapping, so it never widens the page at 320. */
.note-proof-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch-min);
  overflow-wrap: anywhere;
}
/* The lineage (Story 5.3): *Procede del albarán* and *Dividido en* name notes by number, each a
   link to its own detail - a --touch-min target, wrapping anywhere because a child's suffix is the
   whole difference between siblings and is never elided. */
.note-lineage-link {
  display: inline-flex;
  align-items: center;
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  overflow-wrap: anywhere;
}
/* An address is long and unbroken: it wraps rather than widening the column past the screen. */
.requests-queue-operator, .request-detail-number { overflow-wrap: anywhere; }
/* *Detalle de petición* (Story 4.5): the address is a fact, not a column - it wraps anywhere
   rather than widening the page at 320. */
.request-detail-address { overflow-wrap: anywhere; }
/* The decisions: one block each, a heading each, under the facts. */
.request-decisions { max-width: var(--measure); margin-block-end: var(--space-8); }
.request-decision + .request-decision { margin-block-start: var(--space-6); }
.request-decision > h2 { margin-block-end: var(--space-3); }
.request-decision-notice { max-width: var(--measure); color: var(--color-ink-muted); }
/* Operators are a list of addresses, one per line: a row of them wrapping mid-list would read as
   a sentence. Each address may wrap rather than widen the page. */
.request-decisions .choice-group { flex-direction: column; align-items: flex-start; }
.request-decisions .choice-group label > span:last-child { overflow-wrap: anywhere; }

/* ── Divisiones (Story 5.2) ───────────────────────────────────────────────────────────────── */

/* Two sections, *Por decidir* and *Aprobadas, pendientes de dividir*, the second a full --space-8
   below the first so the two lists read as two - *Preparar*'s spacing. The cards are `.note-card`s
   that hold their own controls, so only what a split card adds is here: the pills take their own
   width and wrap (*APROBADA PARA DIVIDIR* is the long one), and the Client's text wraps anywhere
   and keeps its line breaks. */
.split-section { margin-block-start: var(--space-6); }
.split-section + .split-section { margin-block-start: var(--space-8); }
.split-section > h2 { margin-block-end: var(--space-3); }
.split-card-pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
}
.split-card-note { display: flex; flex-direction: column; }
.split-card-note-label { color: var(--color-ink-muted); }
.split-card-note-text {
  overflow-wrap: anywhere;
  white-space: pre-line;
}
.split-card-actions { margin-block-start: var(--space-2); }
/* *Dividir albarán* (Story 5.3): one fieldset per child, each a full --space-6 below the last so
   the children read as separate notes, its legend the child's heading. The parent's Line
   descriptions are long free text and label a field each: they wrap anywhere rather than widen
   the page at 320. The count controls sit under the commit, a row that wraps. */
.split-execute-intro { max-width: var(--measure); margin-block: var(--space-4); }
.split-child { margin-block-start: var(--space-6); }
.split-child > legend {
  font-size: var(--text-heading-size);
  font-weight: var(--text-heading-weight);
  line-height: var(--text-heading-line);
  margin-block-end: var(--space-3);
  overflow-wrap: anywhere;
}
.split-child label { overflow-wrap: anywhere; }
.split-execute-count { margin-block-start: var(--space-3); }

/* ── Preparar and the Preload (Story 4.3) ─────────────────────────────────────────────────── */

/* *Preparar*'s two sections, each under its own heading, the second a full --space-8 below the
   first so the two lists read as two. The cards are `.note-card`s - the whole card the target,
   --touch-critical on the phone - so only what a Request card adds is here. */
.preparation-section { margin-block-start: var(--space-6); }
.preparation-section + .preparation-section { margin-block-start: var(--space-8); }
.preparation-section > h2 { margin-block-end: var(--space-3); }
/* The Client's name and the Operator's address are long free text: they wrap rather than widen
   the column past the phone. */
.request-card-client, .request-card-operator { overflow-wrap: anywhere; }
.request-card-operator { color: var(--color-ink-muted); }

/* The progress, in the identifier face so `2 / 5` does not dance as it counts. Its own element,
   outside the Lines' list, because it is the live region Story 4.4's row swaps update. Positioned,
   so the move sentence a command hides in it stays inside it. */
.preload-progress {
  position: relative;
  font-family: var(--font-mono);
  font-size: var(--text-data-lg-size);
  font-weight: var(--text-data-lg-weight);
  line-height: var(--text-data-lg-line);
  font-variant-numeric: tabular-nums;
}
.preload-summary {
  margin-block-end: var(--space-4);
  color: var(--color-ink-muted);
  overflow-wrap: anywhere;
}
/* Why nothing can be tapped, or why there is nothing to pick: one sentence at the reading
   measure, outlined in the information tone - it explains, it does not warn - and wrapping, an
   Operator's address included. */
.preload-notice {
  max-width: var(--measure);
  margin-block-end: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: var(--border-hairline) solid var(--color-info);
  border-radius: var(--rounded-md);
  overflow-wrap: anywhere;
}
/* {components.preload-line-row} (DESIGN.md:228-241, :461): one row per Line, --touch-critical
   tall and --touch-gap apart on every width - the row is what Story 4.4 makes the target. The
   marker is --touch-min and decoration. A complete row reads in the done foreground with a filled
   marker; **never opacity**, which took the mockup's metadata under the contrast floor
   (review-accessibility.md:133). Positioned, so the visually hidden state word stays inside it. */
.preload-lines {
  display: flex;
  flex-direction: column;
  gap: var(--touch-gap);
  max-width: var(--measure);
  margin: 0;
  padding: 0;
  list-style: none;
}
.preload-line {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-critical);
  padding: var(--space-3);
  background: var(--color-surface-raised);
  color: var(--color-ink);
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-md);
}
.preload-line-marker {
  display: grid;
  flex: none;
  place-items: center;
  width: var(--touch-min);
  height: var(--touch-min);
  border: var(--border-strong) solid var(--color-border-strong);
  border-radius: var(--rounded-sm);
  color: var(--color-ink-muted);
}
.preload-line-done { color: var(--color-ink-muted); }
.preload-line-done .preload-line-marker {
  background: var(--color-success);
  border-color: var(--color-success);
  color: var(--color-ink-inverse);
}
.preload-line-text {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
.preload-line-description { font-weight: var(--text-heading-weight); overflow-wrap: anywhere; }
.preload-line-meta { color: var(--color-ink-muted); overflow-wrap: anywhere; }
/* The tap (Story 4.4). For the assigned Operator each row is a form whose one button **is** the
   row: the same `.preload-line` box, which already gives it its colour, background, border,
   padding and height. What this rule adds resets the control into that box: no native button
   look (`appearance: none`) and no UA margin, the font inherited, start-aligned text, the full
   width of the list, and `touch-action: manipulation`, so a quick second tap is a tap and never a
   zoom. The <li> holds the form, and the form holds the row, the move sentence under it and,
   before it, the shared error on a failure. The focus ring is the global `:focus-visible` one,
   never removed. */
.preload-line-item { min-width: 0; }
.preload-line-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.preload-line-button {
  width: 100%;
  margin: 0;
  font: inherit;
  text-align: start;
  appearance: none;
  touch-action: manipulation;
}
/* {components.preload-line-row}'s other two states (DESIGN.md:240-241): in flight, a hairline in
   the information tone while the command is on its way; failed, a strong border in the danger
   tone once it did not arrive - the padding giving back what the wider border takes, so the row
   never jumps. */
.preload-line[data-state="in-flight"] { border-color: var(--color-info); }
.preload-line[data-state="failed"] {
  padding: calc(var(--space-3) - var(--border-strong) + var(--border-hairline));
  border: var(--border-strong) solid var(--color-danger);
}
/* An automatic transition, said under the row just tapped: where the person is looking
   (EXPERIENCE > *a confirmation of an automatic transition renders where the user is looking*). */
.preload-line-moved {
  max-width: var(--measure);
  font-weight: var(--text-heading-weight);
  overflow-wrap: anywhere;
}

/* The Preload's primary action (Story 4.6): *Generar albarán de salida*, after the Lines, at the
   reading measure. The button is --touch-critical (`.critical`) and full width on the phone. A
   disabled one is waiting for the last Line, not busy: it says so in its hint, so the cursor is
   the plain refusal, never `progress`. */
.preload-dispatch { max-width: var(--measure); margin-block-start: var(--space-6); }
.preload-dispatch:empty { display: none; }
.preload-dispatch-form { display: flex; flex-direction: column; gap: var(--space-2); }
.preload-dispatch-button { width: 100%; }
.preload-dispatch-button:disabled { cursor: not-allowed; }
.preload-dispatch-moved { font-weight: var(--text-heading-weight); overflow-wrap: anywhere; }
/* A Pickup's Preload (Story 4.9): the slot under the summary, and the region's one sentence. */
.preload-slot { margin-block-end: var(--space-4); overflow-wrap: anywhere; }
.preload-dispatch-pickup { overflow-wrap: anywhere; }

/* ── The Exit Delivery Note (Story 4.6) ──────────────────────────────────────────────────── */

/* ROS's paper *ORDEN DE SERVICIO / ALBARÁN DE ENTREGA*, as a page: a bordered sheet at the reading
   measure, its fields in a two-column grid that falls to one on a narrow screen, and blank
   write-in fields - *Matrícula*, *Conductor*, *Peso* - drawn as an empty line to write on. Every
   value wraps: an address, a description or a long reference never pushes the page sideways. */
.exit-note {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  max-width: var(--measure);
  padding: var(--space-6);
  background: var(--color-surface-base);
  color: var(--color-ink);
  border: var(--border-hairline) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
  overflow-wrap: anywhere;
}
.exit-note-header { display: grid; gap: var(--space-2); text-align: center; }
.exit-note-title { margin: 0; }
.exit-note-fields, .exit-note-totals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: var(--space-3) var(--space-6);
  margin: 0;
}
.exit-note-field { display: grid; gap: var(--space-1); min-width: 0; }
.exit-note-field dt {
  color: var(--color-ink-muted);
  font-size: var(--text-label-size);
  font-weight: var(--text-label-weight);
  line-height: var(--text-label-line);
  letter-spacing: var(--text-label-tracking);
}
.exit-note-field dd { margin: 0; }
.exit-note-write-in dd {
  min-height: var(--touch-min);
  border-block-end: var(--border-hairline) solid var(--color-border-strong);
}
.exit-note-parties {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  gap: var(--space-6);
}
.exit-note-party > h2, .exit-note-detail > h2 { margin-block-end: var(--space-2); }
.exit-note-party-name { font-weight: var(--text-heading-weight); }
.exit-note-note + .exit-note-note { margin-block-start: var(--space-6); }
.exit-note-note-heading { margin-block-end: var(--space-1); }
.exit-note-note-meta { margin-block-end: var(--space-2); color: var(--color-ink-muted); }
.exit-note-description { overflow-wrap: anywhere; }
/* A brand is a word in the text face (Story 2.8), but up to 100 characters with no space in it
   must still wrap inside the heading on the narrowest phone and on paper. */
.exit-note-brand { overflow-wrap: anywhere; }
.exit-note-signature { display: grid; gap: var(--space-2); justify-items: center; }
.exit-note-signature-label { font-weight: var(--text-heading-weight); }
.exit-note-signature-box {
  width: 100%;
  min-height: calc(var(--touch-critical) * 2);
  border-block-end: var(--border-hairline) solid var(--color-border-strong);
}
.exit-note-footer {
  padding-block-start: var(--space-3);
  border-block-start: var(--border-hairline) solid var(--color-border);
  color: var(--color-ink-muted);
  text-align: center;
}
.exit-note-offices { margin: 0; padding: 0; list-style: none; }
.exit-note-actions { margin-block-start: var(--space-6); }

/* ── Entregar and the Proof of Delivery (Story 4.7) ───────────────────────────────────────── */

/* The capture screen is the one light Operator screen: <body data-theme="light"> takes the light
   values back inside the warehouse query (tokens.css), so everything below reads the ordinary
   semantic tokens. Its header carries the code and, at the other end, *Modo exterior*. */
.capture-header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  justify-content: space-between;
}
.capture-exterior { margin: 0; }
.capture-exterior [hidden] { display: none; }
.capture-facts { margin-block: var(--space-4); }
/* The privacy notice sits above the pad, before anybody signs, at the reading measure. */
.capture-privacy {
  max-width: var(--measure);
  margin-block: var(--space-4) var(--space-6);
  padding: var(--space-3) var(--space-4);
  border-inline-start: var(--border-strong) solid var(--color-info);
  background: var(--color-surface-sunken);
}
/* Room below *Confirmar entrega*: it is the page's last control, and the live region's toast
   (`.status`, fixed at the bottom) says *Hemos recuperado...* over exactly that spot on a phone
   after a restore. The page can always scroll the button clear of it. */
.capture-form { max-width: var(--measure); padding-block-end: calc(var(--space-12) * 3); }
/* DESIGN.md components.signature-pad: --signature-pad-min-height, a hairline boundary, and paper
   that is white with dark ink **whatever theme the page is in** - the canvas reads its own
   `background-color` and `color` (signature.js), and the stored PNG is a signature on paper.
   `touch-action: none`, so a stroke never scrolls the page. */
.signature { margin-block-end: var(--space-6); }
.signature-pad {
  border: var(--border-interactive) solid var(--color-border-strong);
  border-radius: var(--rounded-md);
  background: var(--color-surface-base-light);
  overflow: hidden;
}
.signature-canvas {
  display: block;
  width: 100%;
  height: var(--signature-pad-min-height);
  touch-action: none;
  background-color: var(--color-surface-base-light);
  color: var(--color-ink-light);
  cursor: crosshair;
}
.signature-status { min-height: calc(var(--text-body-line) * 1em); margin-block-start: var(--space-2); }
.signature-actions { margin-block-start: var(--space-3); }
.signature-actions [hidden] { display: none; }
/* The control armed for its second tap reads as a warning, never as the button it was. */
.signature-actions [data-armed] { border-color: var(--color-danger); color: var(--color-danger); }
/* *Pendiente de subir, reintentando…* (app.js behaviour 14): a state, said plainly - not red,
   because nothing failed that anybody did. */
.capture-pending {
  margin-block: var(--space-4);
  font-weight: var(--text-heading-weight);
  color: var(--color-ink);
}
.capture-pending[hidden] { display: none; }

/* *Modo exterior* (app.js behaviour 15): the light screen's high-contrast variant for direct sun.
   The muted ink and every boundary go to full ink, and the pad's frame to the emphasis width. No
   new colour: the light ink itself. */
[data-theme="light"][data-exterior="on"] {
  --color-ink-muted: var(--color-ink-light);
  --color-border: var(--color-ink-light);
  --color-border-strong: var(--color-ink-light);
  --color-surface-sunken: var(--color-surface-base-light);
}
[data-exterior="on"] .signature-pad { border-width: var(--border-strong); }
[data-exterior="on"] .capture-privacy { border-inline-start-width: var(--focus-ring-width); }

/* The proof, on both surfaces: the retention line, when, the signature on its paper and the
   photos of the goods in the gallery's grid. View-only in place: no download, share or print
   affordance (EXPERIENCE > Privacy). The signature keeps its white paper in the dark warehouse
   too, so the ink reads. */
.proof-retention { max-width: var(--measure); color: var(--color-ink-muted); }
.proof-when { margin-block: var(--space-3); }
.proof-signature { margin: 0; margin-block: var(--space-4); }
.proof-signature-image {
  display: block;
  max-width: 100%;
  width: calc(var(--thumbnail-size) * 5);
  height: auto;
  border: var(--border-hairline) solid var(--color-border);
  border-radius: var(--rounded-sm);
  background: var(--color-surface-base-light);
}
.proof-caption { margin-block-start: var(--space-2); color: var(--color-ink-muted); }
.proof-photos-heading { margin-block: var(--space-6) var(--space-3); }

/* ── Entrega fallida (Story 4.8) ──────────────────────────────────────────────────────────── */

/* The capture's way out, after its form and never inside it: behaviour 14 hides it while a
   delivery is pending upload, so `hidden` has to win over the button's own display. */
.capture-alternative { margin-block-start: var(--space-8); }
.capture-alternative [hidden] { display: none; }
/* The reasons, one per row: each is read before it is tapped, and five side by side on a phone
   would wrap mid-sentence. The label wraps rather than eliding. */
.failure-reasons .choice-group { flex-direction: column; align-items: flex-start; }
.failure-reasons .choice-group label > span:last-child { overflow-wrap: anywhere; }
/* Free text typed at a van door: kept as typed, line breaks included, and never elided. */
.failure-text { overflow-wrap: anywhere; white-space: pre-line; }
.failure-terminal { max-width: var(--measure); margin-block-start: var(--space-4); }

/* ── Entregar al cliente (Story 4.10) ─────────────────────────────────────────────────────── */

/* The Collector's three facts, above the pad: the capture's own fields, spaced like its sections.
   A name or a document number typed at the counter wraps rather than widening the page. */
.collector { margin-block-end: var(--space-6); }
.collector-document-type .choice-group label > span:last-child { overflow-wrap: anywhere; }
.proof-collector { margin-block-end: var(--space-4); }
.proof-collector dd { overflow-wrap: anywhere; }

/* ── Consequence dialog (DESIGN.md components.consequence-dialog) ─────────────────────────── */

/* The only shadowed, scrim-backed element in the product, which is what makes it read as
   genuinely interrupting. Rationed to an irreversible or financial action. */
.consequence-dialog {
  width: calc(100% - var(--space-8));
  /* Never wider than the viewport it interrupts. A modal dialog's `%` resolves against the
     initial containing block and `dvw` against the visual viewport; on a phone the second is
     the smaller of the two, and the reading measure is what caps it on a desktop. */
  max-width: min(var(--measure), calc(100dvw - var(--space-8)));
  padding: var(--space-6);
  background: var(--color-surface-base);
  color: var(--color-ink);
  border: var(--border-strong) solid var(--color-danger);
  border-radius: var(--rounded-lg);
  box-shadow: var(--dialog-shadow);
}
.consequence-dialog::backdrop { background: var(--dialog-scrim); }
.consequence-body { margin-block: var(--space-4) var(--space-6); max-width: var(--measure); }
/* The same body rendered as a page when there is no JavaScript: an open, non-modal <dialog> is
   positioned absolutely by the browser, and in the page it is ordinary content. */
.consequence-dialog[data-standalone] { position: static; width: 100%; }

/* ── Motion ───────────────────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
  html { scroll-behavior: auto; }
}

/* ── The Almacén: the staff chain below `md` (DESIGN.md Layout & Spacing, sm) ─────────────── */

/* Layout and scale only - single column, --touch-critical targets, --touch-gap separation and
   the mobile gutter; the colours changed in tokens.css under this same query. The Portal never
   enters. */
@media (max-width: 767.98px) {
  [data-surface="staff"] .topbar { gap: var(--space-2) var(--space-3); padding-inline: var(--gutter-mobile); }
  [data-surface="staff"] .wordmark-rest { display: none; }
  [data-surface="staff"] .nav-staff { display: none; }
  [data-surface="staff"] .connectivity { padding-inline: var(--gutter-mobile); }
  [data-surface="staff"] .main { padding: var(--space-4) var(--gutter-mobile); }
  [data-surface="staff"] .field { max-width: none; }
  [data-surface="staff"] .tiles { grid-template-columns: 1fr; }
  [data-surface="staff"] .tile,
  [data-surface="staff"] .critical { min-height: var(--touch-critical); }
  [data-surface="staff"] .actions { flex-direction: column; align-items: stretch; }
  [data-surface="staff"] .actions > * { min-height: var(--touch-min); }
  /* **After** the rule above, deliberately (Story 2.2, resolving the Story 2.1 review's
     measurement). Both carry the same specificity, so the later one wins: before this line
     `[data-surface="staff"] .critical` was overridden by `.actions > *`, and *Registrar recepción*,
     *Añadir línea* and *Guardar las fotos* all computed --touch-min where DESIGN.md > Touch Targets
     assigns them --touch-critical. A critical control inside `.actions` is still critical. */
  [data-surface="staff"] .actions > *.critical { min-height: var(--touch-critical); }
  /* Reception (Story 2.1): the Client picker and every Line control are critical targets on the
     warehouse phone - one-handed, with gloves, and nothing depends on hover. The buttons carry
     `.critical` and are already there; the <select> is a field, so it is sized here. */
  [data-surface="staff"] .reception-clients select { min-height: var(--touch-critical); }
  /* Put-away (Story 2.5): DESIGN.md names *Assign Location / make Available* first in the
     Critical class, so every control of this flow is --touch-critical. The commit button carries
     `.critical` and the rule above already sizes it; what is scoped here is the picker <select>,
     which is a field rather than a button, and the card itself - where the target is a row, the
     whole row is the target, and stating it is what keeps it from being wired to the text inside.
     Relocation (Story 2.6) is named in the same class and needs nothing new here: it renders the
     same picker, *Cambiar ubicación* carries `.critical`, and *Últimos ubicados* is the same card. */
  [data-surface="staff"] .putaway-location select { min-height: var(--touch-critical); }
  [data-surface="staff"] .note-card { min-height: var(--touch-critical); }
  /* Story 2.7: the search field takes the phone's whole width, and a fact's label sits over its
     value rather than beside it - two columns on a phone would wrap every value to one word. */
  [data-surface="staff"] .search-field { max-width: none; }
  [data-surface="staff"] .note-fact { grid-template-columns: minmax(0, 1fr); }
  [data-surface="staff"] .line-controls { flex-direction: column; align-items: stretch; }
  [data-surface="staff"] .line-controls > * { min-height: var(--touch-critical); }
  /* One field per row: a description, a quantity and a unit side by side on a phone column are
     three inputs nobody can type into with a gloved thumb. */
  [data-surface="staff"] .line-fields > .field,
  [data-surface="staff"] .line-fields > .field:first-child { flex: 1 1 100%; }
  /* Photos (Story 2.2): the capture control is a critical target and carries `.critical`, so the
     rule above already sizes it. What is scoped here is the block around it - one column, and the
     remove control at --touch-min under each thumbnail, --space-6 away from the critical one. */
  [data-surface="staff"] .photo-capture-actions > * { min-height: var(--touch-critical); }
  [data-surface="staff"] .photo-thumb .button-danger { min-height: var(--touch-min); }
  [data-surface="staff"] .photo-gallery-actions > * { min-height: var(--touch-min); }
  /* The capture (Story 4.7): *Borrar* and *Modo exterior* at --touch-min, never beside the
     critical *Confirmar entrega*, which `.critical` inside `.actions` already sizes. */
  [data-surface="staff"] .signature-actions > *,
  [data-surface="staff"] .capture-exterior > * { min-height: var(--touch-min); }
  /* *Entrega fallida* (Story 4.8): the reasons are its critical choice, so each radio is a
     --touch-critical box at the van door, and the way in from the capture is one too. */
  [data-surface="staff"] .failure-reasons .choice {
    width: var(--touch-critical);
    height: var(--touch-critical);
  }
  [data-surface="staff"] .capture-alternative > * { min-height: var(--touch-critical); }
  /* One column: the row's text, then its two controls stacked full width, each --touch-min and
     --touch-gap apart, so a gloved thumb never lands on Descartar aiming for Continuar.
     Unscoped, unlike everything above it: the Portal carries this section too and reflows all
     the way down (WCAG 1.4.10), so a Client on a phone gets the same separation - and the .actions
     column rule above is staff-only, which is why the stacking is restated here. */
  .unfinished-text,
  .unfinished-actions { flex: 1 1 100%; }
  .unfinished-actions { flex-direction: column; align-items: stretch; }
  .unfinished-actions > * { min-height: var(--touch-min); }
  /* The dark scrim, which is not a semantic role in tokens.css: --dialog-scrim and
     --dialog-scrim-dark are the two frontmatter literals, and only this query picks the second. */
  [data-surface="staff"] .consequence-dialog::backdrop { background: var(--dialog-scrim-dark); }
  /* The row is the target: a cell's controls stack rather than shrink to fit a phone column. */
  [data-surface="staff"] .data-table th, [data-surface="staff"] .data-table td {
    padding: var(--space-3);
  }
  /* Type floor (DESIGN.md:391, :503): the body face is the floor for reading text on an operator
     surface, and body-sm is "never for an instruction or an error". The smaller label and data
     faces are the Back office's; on the warehouse phone a field label and a state pill are read
     at arm's length, with gloves. The :root tokens stay the frontmatter's verbatim - only this
     surface rescales, in the same query that rescales its targets. */
  [data-surface="staff"] {
    --text-label-size: var(--text-body-size);
    --text-data-size: var(--text-body-size);
  }
}

/* ── Print (Story 4.6) ──────────────────────────────────────────────────────────────────── */

/* Printing is for the Exit Delivery Note: the paper is the page. The chrome of both chains - the
   bars, the menus, the connectivity line, the live region, the skip link, the footer - and the
   page's own actions are hidden, the sheet loses its frame and its padding, and the warehouse's
   dark values give way to the light ones, whatever width the printer reports: ink on paper. */
@media print {
  [data-surface="staff"], [data-surface="portal"] {
    --color-ink: var(--color-ink-light);
    --color-ink-muted: var(--color-ink-muted-light);
    --color-surface-base: var(--color-surface-base-light);
    --color-surface-raised: var(--color-surface-base-light);
    --color-border: var(--color-border-light);
    --color-border-strong: var(--color-border-strong-light);
  }
  .skip-link, .topbar, .appbar, .footer, .connectivity, .status, .exit-note-actions { display: none; }
  .main, [data-surface] .main { padding: 0; }
  .exit-note { max-width: none; padding: 0; border: 0; }
  .exit-note .table-wrap { overflow: visible; }
  /* A long note runs over pages; a Line, a note's heading with what follows it, and the signature
     block never split. */
  .exit-note-lines tr { break-inside: avoid; }
  .exit-note-note-heading, .exit-note-note-meta { break-after: avoid; }
  .exit-note-signature { break-inside: avoid; }
}
@page { margin: 15mm; }
