/* ===========================================================================
   Inter-shop transfers — the tab shell, the two lists, the compose screen, the
   review-and-decide screen and the transfer settings dialog.

   Linked by index.html, render-blocking and after review.css, with the
   data-if-sheet="transfers" marker transfers/view.js guards on before linking it
   as the fallback. The module's href is resolved against its own URL, so it
   stays inside the versioned bundle the server serves the build under.

   Every value here comes from the token set in styles.css. No literal colour,
   no raw z-index, and no fixed width on anything holding a translated string:
   these sentences are authored in English and shipped in French, where half of
   them run a third longer and none of them may clip. A transfer is not a new
   visual language — it is the same one pointed at different data — so the four
   tokens below alias existing ones rather than introducing a second palette.
   =========================================================================== */

:root {
  /* The two directions. They are deliberately the app's existing progress and
     done hues rather than a transfer accent of their own: a second brand colour
     makes one app look like two and buys nothing a word in a badge does not
     already say. */
  --tf-out-edge: var(--state-progress-edge);
  --tf-in-edge: var(--state-done-edge);
  /* The line grid, redefined per variant below and again inside the two
     breakpoints, so a column change is one value and never six rules. */
  --tf-cols: minmax(0, 1fr) 88px 110px 110px 110px;
  --tf-queue-width: 288px;
  --tf-match-width: 280px;
  /* --tf-line-pad is gone. A transfer line is a dense row like the settled
     table's, so it reads the shared row tokens; a sheet-local alias in front of
     them only hid which rung the line was on. */
}


/* ── 1. The tab shell ───────────────────────────────────────────────────── */
/* The tabs and the page actions share the kit tabs row (styles.css). */

/* The status column carries a whole sentence naming the other shop
   ("Waiting for St-Laurent"), so it is given room to wrap onto two lines
   instead of being truncated into a word that no longer says who. */
.tf-col-status { min-width: 150px; }
.tf-col-status .badge { white-space: normal; }
/* The lists draw their outcome as the kit's .state chip, which styles.css
   lets wrap between its words: "Reçu par Boulangerie Demo - Verdun" takes two
   lines of its column, never one letter per line. */
/* A transfer number is whatever the sending shop's template made it, and one
   of them is a single 34-character word ("papineauToBoulangerieDemoP04.10.26",
   305px) that held the history table 180px wider than the panel. It may break
   anywhere, but never narrower than a short number reads whole, so "TR-0042"
   stays one piece instead of splitting at its hyphen. A date is one stamp and
   never wraps ("4 oct. / 2026" took three lines at 1280). */
.tf-list .tf-cell-number { overflow-wrap: anywhere; min-width: calc(var(--space-12) * 2 + var(--space-4)); }
.tf-list .tf-cell-when { white-space: nowrap; }
/* Below 900px each row of the three lists is a card: the shop on the first
   line beside its tick, every figure under it on a line of its own with its
   column's name, and the row's actions across the full width at the bottom,
   where "Vérifier et décider" sat 400px past the right edge of a phone (and
   just past it at 768) behind a sideways scroll. The shared .table-stack
   (styles.css) draws the card below 720px; these lists ask for it from bp-mid,
   because eight columns, one of them a sentence, do not fit the 692px a
   portrait tablet leaves either, so the utility's few rules are said again
   here at that width, in the same values, and the two agree wherever both
   apply. */
@media (max-width: 900px) {
  .tf-list.table-stack { min-width: 0; width: 100%; }
  .tf-list.table-stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .tf-list.table-stack, .tf-list.table-stack tbody { display: block; }
  .tf-list.table-stack tbody tr {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-1) var(--space-3);
    padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--rule);
  }
  .tf-list.table-stack tbody tr:last-child { border-bottom: 0; }
  .tf-list.table-stack tbody td {
    grid-column: 1 / -1;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-2);
    height: auto; padding: 0; border: 0; text-align: left; min-width: 0;
    overflow-wrap: anywhere;
  }
  .tf-list.table-stack tbody td[data-label]::before {
    content: attr(data-label); flex: 0 0 auto; min-width: 7.5rem;
    color: var(--ink-muted); font-size: var(--text-xs);
  }
  .tf-list.table-stack tbody td:empty { display: none; }
  .tf-list.table-stack tbody td.tf-cell-title { grid-row: 1; grid-column: 1; font-weight: var(--weight-semibold); color: var(--ink-strong); }
  .tf-list.table-stack tbody td.col-select { grid-row: 1; grid-column: 2; width: auto; justify-content: flex-end; }
  .tf-list.table-stack tbody td.col-actions { padding-top: var(--space-1); }
  .tf-list.table-stack tbody td.col-actions > .btn { flex: 1 1 auto; }
  .tf-list.table-stack tbody td.col-actions .cell-actions { flex: 1 1 100%; justify-content: flex-start; }
  .tf-list .tf-cell-number { min-width: 0; }
  /* A row's opened lines are a block of their own under its card. */
  .tf-list.table-stack tbody tr.tf-hist-detail > td, .tf-list.table-stack tbody tr.tf-detail-row > td { display: block; }
}


/* ── 2. Loading, error and offline ──────────────────────────────────────── */
/* Skeleton rows inside the table body, not the blocking overlay: the overlay is
   reserved for POS writes, where blocking the whole app is the correct thing to
   do. Reading a list is not one of those. */
/* The shine is the shared skeleton's (styles.css section 10): a static
   gradient on a pseudo-element slid across by transform, which the compositor
   runs, where the background-position sweep it replaces repainted every row of
   the table every frame. It stops after the shared number of passes, like
   every other skeleton: a read that has not answered by then is a hung one. */
.tf-skel {
  position: relative; overflow: hidden;
  display: block; height: var(--text-base); width: 100%;
  border-radius: var(--radius-xs);
  background: var(--surface-sunken);
}
.tf-skel::after {
  content: ""; position: absolute; inset: 0;
  background-image: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent);
  transform: translateX(-100%);
  animation: if-sweep var(--dur-sweep) linear var(--sweep-passes) both;
}
.tf-skel-row td { padding: var(--space-3); }

.tf-error {
  display: flex; align-items: flex-start; gap: var(--space-3); flex-wrap: wrap;
  padding: var(--space-4);
  border: 1px solid var(--state-blocked-rule);
  border-radius: var(--radius-md);
  background: var(--state-blocked-bg); color: var(--state-blocked-fg);
  box-shadow: var(--edge) var(--state-blocked-edge);
}
.tf-error-body { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.tf-error-line { margin: 0; font-weight: var(--weight-semibold); }
.tf-error .btn { margin-left: auto; }

.tf-offline {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  margin: 0;
  font-size: var(--text-xs); color: var(--state-offline-fg);
}


/* ── 3. The callout ─────────────────────────────────────────────────────── */
/* The point of the outgoing detail panel. Two of the sentences it holds ask the
   operator to go and enter a correcting document in the POS, which is why this
   is a persistent block and never a toast: a decline does not un-file the
   sender's document, and a person who does not know that has a stock count that
   is quietly wrong. */
/* Drawn by the kit callout (.banner.ui-callout and its tone, styles.css); the
   persistent block keeps its sentences stacked with room between them. */
.tf-callout { align-items: flex-start; }
.tf-callout .banner-message { gap: var(--space-2); }
.tf-callout p { margin: 0; line-height: var(--leading-loose); }
.tf-callout .tf-foot-actions { margin: 0; }

/* The operator's own words, in whatever language they typed them. Never
   translated and never restyled into the surrounding sentence, so a reader can
   see where our copy stops and theirs starts. */
.tf-quote {
  margin: 0; padding: var(--space-2) var(--space-3);
  border-left: 2px solid currentColor;
  font-style: italic; line-height: var(--leading-loose);
  overflow-wrap: anywhere;
}


/* ── 4. The line grid ───────────────────────────────────────────────────── */
/* One rhythm for compose, the outgoing detail and the decide screen, and a wrapper
   that names itself the pane so a row collapses on the width it has, not the window's. */
.tf-lines { display: flex; flex-direction: column; min-width: 0; container-type: inline-size; container-name: pane; }
.tf-lines--wide { --tf-cols: minmax(0, 1fr) 88px 110px 110px 110px minmax(140px, .8fr); }
.tf-lines--edit { --tf-cols: minmax(0, 1fr) 96px 120px 120px 110px 48px; }
/* The article name is what the operator reads first: it takes the room, the
   figures stay compact and right-aligned, and the catalogue answer keeps enough
   width for one product name and its Changer. */
.tf-lines--review { --tf-cols: minmax(0, 1.6fr) 64px 96px 100px 110px minmax(240px, 1.3fr); }
/* Every line settled by this shop's own catalogue (REDESIGN 4.6): the evidence
   column folds to the width of its one control, and the sentence that replaces
   it sits above the table (.tf-folded-note). */
.tf-lines--folded { --tf-cols: minmax(0, 1fr) 88px 110px 110px 110px max-content; }

.tf-line {
  display: grid; grid-template-columns: var(--tf-cols);
  align-items: center; gap: var(--space-3);
  padding: var(--row-pad-y-dense) var(--row-pad-x-dense);
  border-bottom: 1px solid var(--rule);
  min-width: 0;
}
.tf-line:last-child { border-bottom: 0; }
.tf-line--head {
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--rule-strong);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  color: var(--ink-muted);
}
.tf-line > .num { text-align: right; }
.tf-line .tnum { font-variant-numeric: tabular-nums lining-nums; }

/* The item name is the one thing allowed to ellipse, and only because the whole
   string stays reachable in its title attribute. */
.tf-line-name {
  min-width: 0; display: flex; flex-direction: column; gap: 1px;
  overflow: hidden; text-overflow: ellipsis;
  color: var(--ink-strong);
}
.tf-line-sku { font-size: var(--text-2xs); color: var(--ink-muted); }
.tf-line-unit { font-size: var(--text-xs); color: var(--ink-muted); min-width: 0; }
.tf-line-total { font-weight: var(--weight-medium); color: var(--ink-strong); }
.tf-line-actions { display: flex; justify-content: flex-end; }
.tf-line-extra { min-width: 0; }
.tf-lines-empty { margin: var(--space-4) 0; }

.tf-line .qty-input, .tf-line .price-input { text-align: right; }

/* The compose lines: the review's settled table, in a card like the picker
   above it. styles/review.css is linked by index.html ahead of this sheet, and
   transfers/view.js links it only as the fallback. This table does not restate
   the review's column widths: it narrows or widens them through the --stl-*
   properties set on .tf-table below, and the hover-only field chrome and the
   zebra come from .stl-row. */
.tf-lines-card { border: 1px solid var(--rule); border-radius: var(--radius-md); background: var(--surface-raised); overflow: hidden; }
/* Widths, the quantity field and heading wrap go to the review's rules as
   properties, never restated at equal specificity, where the last sheet linked
   would win (review.css reads them). Headings may take two lines ("Total de la
   ligne", "Prix unitaire"); the actions column holds change-product
   (docs/production/14) and remove. The price field keeps the review's width, so
   it is not named here at all. The last three are the spacing of a stacked row:
   the review stacks this table in a narrow pane and the collapse at 620px below
   stacks it too, and both read the same values. */
.tf-table { --stl-c-qty: 88px; --stl-c-unit: 132px; --stl-c-price: 148px; --stl-c-amount: 124px;
  --stl-c-actions: 84px; --stl-qty-input: 72px; --stl-th-wrap: normal;
  --stl-row-gap: var(--space-2) var(--space-4); --stl-row-pad: var(--row-pad-y) var(--row-pad-x);
  --stl-actions-flex: 1 0 100%; }
.tf-table th { line-height: var(--leading-tight); vertical-align: bottom; }
/* The change control is a toggle and takes the pressed surface every other icon
   toggle takes, from styles.css, so "the picker is open on this line" reads the
   same as every other "on" in the app. */
.tf-table td.stl-actions { display: flex; justify-content: flex-end; gap: var(--space-1); }
.tf-table .tf-line-name { display: flex; flex-direction: column; gap: 1px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-strong); }
.tf-table .unit-inline { width: 100%; max-width: 120px; }
.tf-price-cell { display: inline-flex; align-items: center; justify-content: flex-end; gap: var(--space-1); width: 100%; }
/* The picker's own "POS" tag beside a price the POS supplied; it goes the
   moment the price is typed over. */
.tf-pos-tag { font-size: var(--text-2xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--ink-muted); }
.tf-pos-tag[hidden] { display: none; }
@media (max-width: 620px) {
  /* The table becomes a list of wrapping rows, every cell carrying its own
     label, the way the other transfer screens already do at this width. */
  .tf-table thead { display: none; }
  .tf-table, .tf-table tbody, .tf-table tr, .tf-table td { display: block; }
  .tf-table colgroup { display: none; }
  /* The gap, the padding and the full-width actions cell restate the values
     .tf-table hands the review's own stacked layout above, so wherever both
     layouts apply they agree and the order of the two sheets decides nothing. */
  .tf-table tr.stl-row { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); padding: var(--row-pad-y) var(--row-pad-x); border-bottom: 1px solid var(--rule); }
  .tf-table tr.stl-row > td { border: 0; padding: 0; background: transparent; text-align: left; }
  .tf-table td.tf-cell-item { flex: 1 0 100%; }
  .tf-table td[data-k]::before { content: attr(data-k); display: block; font-size: var(--text-2xs); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--ink-muted); }
  .tf-table td.stl-actions { flex: 1 0 100%; text-align: left; }
  .tf-price-cell { width: auto; justify-content: flex-start; }
}
/* The price the POS last knew is a starting point, not an authority: it is
   marked as borrowed and the chip disappears the moment it is typed over. */
.tf-chip { margin-top: var(--space-1); }


/* ── 5. Compose ─────────────────────────────────────────────────────────── */
.tf-compose { display: flex; flex-direction: column; gap: var(--space-5); }

/* Wrapping flex rather than a fixed column count: "Send to" becomes "Envoyer
   à", "Transfer number" becomes "Numéro de transfert", and a French label that
   runs a third longer must push the row down, never clip. */
.tf-head-card {
  display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface-raised);
}
.tf-field { flex: 1 1 220px; min-width: 220px; }
.tf-field + .tf-field { margin-top: 0; }
.tf-field--wide { flex-basis: 100%; }

/* Radios, not a switch: a switch has nowhere to put the consequence sentence,
   and the meaning of this control depends on which end of the transfer you are
   reading it from. */
.tf-direction { border: 0; padding: 0; margin: 0; min-width: 220px; }
.tf-direction legend { padding: 0; }
.tf-radio {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: var(--text-sm); color: var(--ink); cursor: pointer;
}
.tf-radio input { width: auto; min-height: 0; margin: 0; }
.tf-direction-note {
  display: flex; align-items: flex-start; gap: var(--space-2);
  margin: var(--space-2) 0 0;
  font-size: var(--text-xs); color: var(--ink-muted); line-height: var(--leading-loose);
}
.tf-direction-note svg { flex: none; color: var(--tf-out-edge); }

.tf-section-head {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  color: var(--ink-muted);
}
/* The heading and the control that puts a "change this line's product" picker
   away sit on one line, wrapping rather than colliding when a product name is
   long (docs/production/14-transfer-manual-matching.md). */
.tf-section-head-row {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); margin: 0 0 var(--space-2);
}
.tf-section-head-row .tf-section-head { margin: 0; }
.tf-picker-field {
  padding: var(--space-4);
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface-raised);
}

/* The total and the button that commits it stay together at the bottom of the
   screen. On a thirty-line transfer the number you are watching would otherwise
   scroll away from the control that files it. */
.tf-foot {
  position: sticky; bottom: 0; z-index: var(--layer-sticky);
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4);
  border-top: 1px solid var(--rule-strong); border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: var(--surface-raised);
  box-shadow: var(--elev-2);
}
/* A short window has no room to keep it there. On a phone held sideways
   (844x390) the pinned footer, its blockers listed, stood 165px tall over the
   70% of the screen the app bar had not taken, and covered the very fields the
   blockers named. Below 500px of height (in em, as styles.css writes its one
   height query) it goes back into the flow at the end of the screen. */
@media (max-height: 31.25em) {
  .tf-foot { position: static; box-shadow: none; border-radius: var(--radius-md); border: 1px solid var(--rule-strong); }
}
.tf-foot-info { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.tf-total {
  font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--ink-strong);
  font-variant-numeric: tabular-nums lining-nums;
}
.tf-foot-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }

/* A disabled button with no explanation is a dead end, so what is missing is
   listed as sentences beside it rather than left to be guessed. */
.tf-blockers {
  margin: 0; padding-left: var(--space-5);
  font-size: var(--text-xs); color: var(--state-attention-fg); line-height: var(--leading-loose);
}

.tf-confirm, .tf-sent {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--rule-strong); border-radius: var(--radius-md);
  background: var(--surface-sunken);
  animation: if-rise var(--dur-3) var(--ease-out) both;
}
.tf-confirm p, .tf-sent p { margin: 0; line-height: var(--leading-loose); }
.tf-confirm-title, .tf-sent-line {
  font-size: var(--text-md); font-weight: var(--weight-semibold); color: var(--ink-strong);
}
.tf-confirm-total { font-size: var(--text-md); font-variant-numeric: tabular-nums lining-nums; color: var(--ink-strong); }
.tf-confirm-warn {
  display: flex; align-items: flex-start; gap: var(--space-2);
  color: var(--state-attention-fg); font-weight: var(--weight-medium);
}
.tf-confirm-warn svg { flex: none; }
.tf-confirm .tf-foot-actions, .tf-sent .tf-foot-actions { margin-top: var(--space-2); }
.tf-sent {
  flex-direction: row; align-items: flex-start; gap: var(--space-3); flex-wrap: wrap;
  border-color: var(--state-done-rule); background: var(--state-done-bg); color: var(--state-done-fg);
}
.tf-sent > div { min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.tf-sent svg { flex: none; }


/* ── 6. The outgoing detail panel ───────────────────────────────────────── */
.tf-detail-row > td { padding: 0; background: var(--surface-sunken); }
.tf-detail {
  display: flex; flex-direction: column; gap: var(--space-4);
  padding: var(--space-4);
  box-shadow: var(--edge) var(--tf-out-edge);
  animation: if-rise var(--dur-3) var(--ease-out) both;
}
.tf-detail .tf-lines {
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface-raised);
}


/* ── 6b. Historique (REDESIGN 4.6) ─────────────────────────────────────── */
/* The search, the direction and the pager are drawn once and patched
   (transfers/history.js), so none of them may change size with its words: the
   pager's count holds the middle and the two buttons hold the ends. */
.tf-hist { display: flex; flex-direction: column; gap: var(--space-4); }
.tf-hist-tools { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-4); }
.tf-hist-field { flex: 1 1 var(--queue-w); min-width: 0; }
.tf-hist-note { margin: 0; }
.tf-hist-side { display: inline-flex; align-items: center; gap: var(--space-2); }
.tf-hist-side svg { color: var(--ink-muted); }
.tf-hist-detail > td { padding: var(--space-4); background: var(--surface-sunken); }
.tf-hist-detail .tf-lines {
  border: 1px solid var(--rule); border-radius: var(--radius-md);
  background: var(--surface-raised);
}
/* At 320 the two buttons and the count came to 4px more than the panel, and
   "Suivants" sat past the screen's edge: the count may take two lines, and if
   that is still not enough the next button goes under, never off. */
.tf-hist-pager { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); }
.tf-hist-count { flex: 1 1 0; min-width: 0; text-align: center; color: var(--ink-muted); font-size: var(--text-sm); }


/* ── 7. Review and decide ───────────────────────────────────────────────── */
/* ONE COLUMN UNTIL bp-xl. Beside the queue the transfer kept what was left of
   the window, and below 1360px that was too little for its own lines: the
   article column measured 16px at 1280 (one word per line, under the quantity)
   and 0px on a phone. Under 1360 the transfer takes the whole width; the
   queue's two panels step aside (the "Retour aux entrants" control right above
   the transfer, and the tab and the breadcrumb, still lead back to the inbox
   they list), so the screen opens on the transfer itself rather than on the
   queue and the outgoing card stacked over it (Accept sat 2000px down at 390).
   The two columns return at bp-xl, read from above as the ladder writes it. */
.tf-review-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-3); align-items: start; }
.tf-review-queue { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); min-width: 0; }
.tf-review-queue > .panel { display: none; }
@media (min-width: 1360px) {
  .tf-review-layout { grid-template-columns: var(--tf-queue-width) minmax(0, 1fr); }
  .tf-review-queue { align-items: stretch; }
  .tf-review-queue > .panel { display: block; }
}
/* The transfer names itself the container its line table asks about (below):
   the window says nothing about what the queue, the rail or the app bar left
   it. */
.tf-review { min-width: 0; container: tfreview / inline-size; }
.tf-review-head { display: grid; grid-template-columns: minmax(0, 1fr) var(--tf-match-width); gap: var(--space-3); }
.tf-review-identity { grid-column: 1 / -1; }
.tf-review-head > .inline-note { grid-column: 1 / -1; }
.tf-review-identity { display: flex; align-items: start; justify-content: space-between; flex-wrap: wrap; gap: var(--space-4); }
.tf-review-identity .ui-summary { flex: 0 1 var(--tf-queue-width); }
.tf-queue-entry { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); padding: var(--row-pad-y) var(--row-pad-x); }
.tf-queue-entry.is-selected { background: var(--surface-selected); box-shadow: var(--edge) var(--accent); }
.tf-review-table td { vertical-align: middle; }
.tf-review-table .tf-line-match { flex-direction: column; align-items: flex-start; }
/* The disc, the answer and "Changer" on one line when the column holds them,
   "Changer" on a line of its own when it does not. Held on one line, the answer
   gave up its width to the button and read one word per line (61px of a 188px
   column at 844), and the button still ran past the column's edge. The answer
   keeps 96px before the button is the one that moves. */
.tf-match-evidence .ui-match-identity { flex-wrap: wrap; }
.tf-match-evidence .ui-match-copy { flex: 1 1 calc(var(--space-12) * 2); }
.tf-match-evidence .tf-line-change { margin-left: auto; }
.tf-match-evidence .state { white-space: normal; }
@media (max-width: 620px) {
  .tf-review-head { grid-template-columns: minmax(0, 1fr); }
}

/* What this shop's own catalogue says about each sent line, AND the controls that
   change it (docs/production/14-transfer-manual-matching.md). The badge and the
   sentence are reading matter — a preview of the match the server will re-check
   against the live template — and the buttons in .tf-line-controls below sit in
   this same cell, because the choice is MADE here. That is why the cell wraps:
   the sentence keeps the width it needs and the controls take a row of their own
   under it rather than competing with it for one column. */
.tf-line-match { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.tf-line-match .small { flex: 1 1 100%; }
.tf-match-chip { max-width: 100%; }
.tf-match-pending { font-style: italic; }
.tf-match-note { margin: 0; }

/* EVERY REVIEW LINE IS ALSO A CONTROL
   (docs/production/14-transfer-manual-matching.md). The badge and the sentence
   above are still reading matter; the two buttons under them are what makes the
   preview actionable, and they take the whole width of the cell so the sentence
   they follow is never squeezed into a column of its own. */
.tf-line-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); flex: 1 1 100%; }
/* The answers to a unit question (gap register D7): the units this shop's own
   item offers, as a row of buttons under the question, laid out like the line's
   other controls so the question reads as part of the line it is about. */
.tf-unit-answers { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); flex: 1 1 100%; }
.tf-match-unit { color: var(--ink-strong); }
/* THE FOLDED LINE'S ONE CONTROL. "Changer" is always in the tab order and keeps
   its box, so revealing it never moves the row; only its paint waits for the
   pointer on the row, or the keyboard inside it. It is clipped the way .sr-only
   clips (nothing dims text, DESIGN_SYSTEM 2.4), and where there is no pointer
   there is no reveal, so it is simply there (DESIGN_SYSTEM 8.4, the hover rule). */
.tf-line-match--folded { justify-content: flex-end; }
.tf-folded-note { display: flex; align-items: center; gap: var(--space-2); }
.tf-folded-note svg { color: var(--state-done-edge); }
/* A line left out is not filed here, so it reads as withdrawn rather than as a
   problem: the row goes quiet, and the badge beside it says why. */
.tf-line--left-out > span:not(.tf-line-match) { color: var(--ink-muted); }
/* The picker needs the width of the table, so it sits in its own strip under
   the line it belongs to rather than inside one grid column. */
.tf-line-pick {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--rule);
  background: var(--surface-sunken);
}
.tf-line-pick-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); }
.tf-line-pick-head .field-label { margin: 0; }

.tf-decline {
  display: flex; flex-direction: column; gap: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--state-attention-rule);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--edge) var(--state-attention-edge);
  animation: if-rise var(--dur-3) var(--ease-out) both;
}
.tf-decline-reasons { border: 0; padding: 0; margin: 0; }
.tf-decline-reasons legend { padding: 0; margin-bottom: var(--space-2); }
.tf-decline-text { min-height: calc(var(--control-h) * 2); resize: vertical; }
.tf-decline-count { font-variant-numeric: tabular-nums lining-nums; }


/* ── 8. The settings dialog ─────────────────────────────────────────────── */
.tf-settings-body { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-5); }
.tf-settings-body .field + .field { margin-top: 0; }
/* The live example is the answer to "what will my transfers be called", so it
   is set in the numeric face the number itself will be shown in. */
.tf-set-example {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink-strong);
  overflow-wrap: anywhere;
}


/* ── 9. Narrow screens ──────────────────────────────────────────────────── */
/* Two stages, both driven by the one column variable. The match column goes
   first because it is the widest and the least urgent; the numbers go last
   because they are what the operator is reading. */
@media (max-width: 900px) {
  .tf-lines--review { --tf-cols: minmax(0, 1fr) 88px 110px 110px 110px;
  --tf-queue-width: 288px;
  --tf-match-width: 320px; }
  .tf-lines--folded .tf-line-match { justify-content: flex-start; }
  .tf-lines--review .tf-line-match,
  .tf-lines--review .tf-line--head > span:last-child { grid-column: 1 / -1; }
  .tf-lines--review .tf-line--head > span:last-child { display: none; }
}

@container pane (max-width: 640px) {
  /* Below this the pane - not the window - stops fitting the grid, so the row
     becomes a wrapping block and every cell carries its own label. A header a row
     no longer lines up with is a lie, so the header row goes with the grid.
     Both classes name it: `.tf-line` on the next line sets every row to flex at
     the same specificity and came later, so the header showed its column names
     as a wrapping row of their own above lines that already carry them. */
  .tf-line.tf-line--head { display: none; }
  .tf-line { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
  .tf-line-name { flex: 1 0 100%; }
  .tf-line > .num, .tf-line > span { text-align: left; }
  .tf-line [data-k]::before {
    content: attr(data-k) " ";
    display: block;
    font-size: var(--text-2xs); font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-caps); text-transform: uppercase;
    color: var(--ink-muted);
  }
  .tf-line-actions { flex: 1 0 100%; justify-content: flex-start; }
  .tf-line-match { flex: 1 0 100%; }
  .tf-field { min-width: 0; flex-basis: 100%; }
}


/* ── 10. Reduced motion ─────────────────────────────────────────────────── */
/* styles.css already kills animation globally under this query; these are named
   again so a future reader of THIS file can see that the shimmer and the rise
   are covered rather than having to go and check. */
@media (prefers-reduced-motion: reduce) {
  .tf-skel, .tf-callout, .tf-confirm, .tf-sent, .tf-detail, .tf-decline { animation: none; }
  .tf-skel::after { display: none; }
}


/* ── 11. What arrived, and the slip (GAP_REGISTER Part 3, transfers) ────── */
/* "Quantités reçues différentes": the toggle and its one sentence above the
   lines, and a field in the quantity cell sized to the figures it holds, so the
   row keeps its height whether the field is open or not. */
.tf-received-bar { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-2); }
.tf-received-qty { width: 7ch; text-align: right; font-variant-numeric: tabular-nums; }
.tf-received-was { text-decoration: line-through; color: var(--ink-muted); }
.tf-received-got { color: var(--state-attention-fg); font-weight: var(--weight-medium); }

/* The transfer slip is drawn only to be printed (transfers/slip.js): never on
   screen, and on paper the only thing there is. The print sheet's rules all
   carry !important because they come first in the cascade; these must outrank
   them, so they carry it too, and the selector names the slip by its id. */
.tf-print { display: none; }
@media print {
  html.is-printing-slip body > :not(#tf-print) { display: none !important; }
  html.is-printing-slip #tf-print { display: block !important; }
  .tf-slip { color: var(--paper-ink) !important; font-size: var(--text-sm) !important; }
  .tf-slip h1 { font-size: var(--text-lg) !important; margin: 0 0 var(--space-3) !important; }
  .tf-slip-meta { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: var(--space-2) var(--space-4) !important; margin: 0 0 var(--space-4) !important; }
  .tf-slip-meta dt { font-weight: var(--weight-semibold) !important; }
  .tf-slip-meta dd { margin: 0 !important; }
  .tf-slip-lines { width: 100% !important; border-collapse: collapse !important; }
  .tf-slip-lines th, .tf-slip-lines td { border-bottom: 1px solid var(--paper-rule) !important; padding: var(--space-1) var(--space-2) !important; text-align: left !important; }
  .tf-slip-lines .num { text-align: right !important; font-variant-numeric: tabular-nums !important; }
  .tf-slip-box { width: 3em !important; border: 1px solid var(--paper-rule) !important; }
  .tf-slip-note { margin: var(--space-3) 0 0 !important; }
  .tf-slip-signs { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: var(--space-6) !important; margin-top: var(--space-6) !important; }
  .tf-slip-signs div { border-top: 1px solid var(--paper-ink) !important; padding-top: var(--space-1) !important; }
}

/* ── 12. Several transfers at once (transfers/bulk.js) ─────────────────────
   The tick column, the bar above each list, and what an accept of several
   answered. The bar is one control height whether anything is ticked or not,
   so the first tick never pushes the table down; the tick cell is one tap
   target wide. Several slips print one to a page. */
.tf-bulk {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3);
  min-height: var(--control-h-sm); margin-bottom: var(--space-2);
}
.tf-bulk-count { color: var(--ink-muted); font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
.tf-bulk-actions { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); }
.col-select { width: var(--tap-min); text-align: center; }
.tf-select { margin: 0; }
.tf-bulk-result { margin-bottom: var(--space-3); }
.tf-bulk-result-list { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.tf-bulk-result-list .state { white-space: normal; text-align: left; }
@media print {
  .tf-slip + .tf-slip { break-before: page !important; }
}

/* The incoming transfer's line table: the article name and the catalogue answer
   share the width, the figures keep compact fixed columns, so a product name
   reads on one line, two at most, at 1440 px.
   THE ARTICLE IS THE REMAINDER, SO THE OTHERS ARE SIZED TO LEAVE ONE. The four
   figures took 384px and the catalogue column a flat 280, 664 in all, and the
   article got whatever the pane had over that: 16px at 1280 beside the queue.
   The figures now take what their longest value needs (104px holds "1 234,56 $"
   with the cell's padding), the catalogue answer a third of the table (a plain
   percentage: a table cell treats a calc() or clamp() mixing % and px as auto),
   which keeps its disc, its answer and "Changer" on one line from 840px of
   table, and the headings wrap onto two lines ("Prix unitaire", "Total de la
   ligne") rather than run under the next one. The narrowest table that is
   still a table (733px, see the stack below) leaves the article 140px: two
   lines. */
.tf-review-table { table-layout: fixed; width: 100%; }
.tf-review-table th { white-space: normal; vertical-align: bottom; }
.tf-review-table th:nth-child(2) { width: calc(var(--space-12) + var(--space-2)); }
.tf-review-table th:nth-child(3) { width: calc(var(--space-12) + var(--space-10)); }
.tf-review-table th:nth-child(4), .tf-review-table th:nth-child(5) { width: calc(var(--space-12) * 2 + var(--space-2)); }
.tf-review-table th:last-child { width: 33%; }
/* "Quantités reçues différentes" puts a field in the quantity cell. styles.css
   sizes a small field in a numeric cell to the cell (`td.num .input--sm`, 100%),
   which in a 48px column left 18px of field and no figure at all; this names
   the field inside this table, one class above it, so its own seven figures
   win, and the column opens to hold them while the fields are there
   (decide.js adds the modifier with the fields). */
.tf-review-table td.num .tf-received-qty { width: 7ch; }
.tf-review-table--received th:nth-child(2) { width: calc(var(--space-12) * 2); }
/* THE STACK. Under 760px of transfer (a phone, the app bar's tablet) the six
   columns no longer leave the article a word, so each line becomes a block,
   the way the compose table does at 620px (section 4): the article across the
   top, the four figures side by side under it, each carrying its column's name
   (data-k, decide.js), and the catalogue's answer with its controls across the
   bottom. The header row goes with the grid: a header no cell lines up with is
   a lie. */
@container tfreview (max-width: 760px) {
  .tf-review-table { table-layout: auto; }
  .tf-review-table thead { display: none; }
  .tf-review-table, .tf-review-table tbody, .tf-review-table tr, .tf-review-table td { display: block; }
  .tf-review-table tr.tf-review-line {
    display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-2) var(--space-5);
    padding: var(--row-pad-y) var(--row-pad-x); border-bottom: 1px solid var(--rule);
  }
  .tf-review-table tr.tf-review-line > td { height: auto; padding: 0; border: 0; text-align: left; }
  .tf-review-table tr.tf-review-line > td:first-child { flex: 1 0 100%; }
  .tf-review-table tr.tf-review-line > td:last-child { flex: 1 0 100%; }
  .tf-review-table td[data-k]::before {
    content: attr(data-k); display: block;
    font-size: var(--text-2xs); font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-caps); text-transform: uppercase;
    color: var(--ink-muted);
  }
}
