/* Component layer — design-system.md §8.
 *
 * Everything here consumes Layer 2 semantic tokens. No literal colour, no Layer 1 primitive:
 * DesignTokenLayerTests fails the build on either, and a token that is not declared anywhere
 * resolves to nothing and drops the declaration silently.
 *
 * LAYER 3 COMPONENT TOKENS are declared at the top of each block, named --sw-*, with the reason
 * they exist. They are for values the design system does not supply - a control height, a card
 * width - never for a colour we felt like inventing. Same rule --sw-drawer-scrim follows in
 * app-shell.css.
 *
 * The three things that make this feel like a product rather than a template, all from tokens
 * that already existed and were unused:
 *   - Type scale used across three sizes on one screen, not everything at base.
 *   - Elevation as hierarchy: --shadow-sm resting, --shadow-md on hover, --shadow-lg for the card.
 *   - Motion at --duration-fast with --easing-standard, inside the prefers-reduced-motion block
 *     base.css already ships.
 */

/* ===================================================================================
 * Layer 3 component tokens
 * ===================================================================================
 *
 * design-system.md §1 permits these only when a component needs a value the semantic layer
 * cannot express, and §1's naming convention applies: --color-* for anything that is a colour.
 */
:root {
  /* --color-auth-page-background is NOT declared here, and the reason is the layering rule
   * rather than taste: its value derives from --color-primary-50, a Layer 1 primitive, and
   * DesignTokenLayerTests forbids referencing a primitive outside tokens.css. It is declared in
   * tokens.css instead, in the Layer 3 block, and consumed here as an opaque token.
   *
   * That is the convention now written into design-system.md §1: a Layer 3 token lives beside its
   * component UNLESS its value derives from a primitive, in which case it lives in tokens.css.
   * The primitive rule is absolute; "where the token is written down" is negotiable.
   *
   * Caught by the build rather than by review - the first version of this file declared it here
   * and the test failed. */

  /* Above everything, including the drawer. Moved here from app-shell.css with the skip link
   * itself: the signed-out layout renders that link and does not load the shell stylesheet,
   * which is why the link was rendering unstyled and in flow on the login page. Not a colour, so
   * no primitive is involved and it belongs beside the component. */
  --sw-z-skip-link: 50;

  /* ABOVE THE PAGE AND THE TOPBAR, BELOW THE DRAWER AND ITS SCRIM — §8.15. The notification is
   * page-level feedback, so content must not cover it; the drawer is a mode the reader entered
   * deliberately and should. A modal dialog beats both without a number, because a native <dialog>
   * opened with showModal() is in the top layer and no z-index reaches it. Scale: skip link 50,
   * sidebar 40, scrim 30, THIS 25, topbar 20. */
  --sw-z-notice: 25;
}

/* ===================================================================================
 * Skip link - design-system.md §9 rule 2
 * ===================================================================================
 *
 * HERE, NOT IN app-shell.css. It is on every page, and the signed-out layout does not load the
 * shell stylesheet - so this rule sitting there left the login page with a visible, unstyled
 * link in normal flow above the card.
 *
 * CLIPPED, NOT HIDDEN. display:none and visibility:hidden both remove an element from the focus
 * order, which is the one thing this element exists to be in. clip-path leaves it focusable and
 * takes it out of sight; the 1px box with overflow:hidden stops the text painting outside the
 * clip in engines that lay it out before clipping.
 */
.sw-skip-link {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  z-index: var(--sw-z-skip-link);

  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;

  border-radius: var(--radius-md);
  background: var(--color-surface-primary);
  color: var(--color-text-primary);
  font-weight: var(--font-weight-medium);
  text-decoration: none;
}

/* Visible, at the top-left of the viewport, with the standard focus ring from base.css on top. */
.sw-skip-link:focus-visible {
  inline-size: auto;
  block-size: auto;
  margin: var(--space-3);
  padding: var(--space-3);
  overflow: visible;
  clip-path: none;
  box-shadow: var(--shadow-md);
}

/* ===================================================================================
 * Icons — §7
 * ===================================================================================
 *
 * HERE RATHER THAN IN app-shell.css, FOR THE SAME REASON AS THE BRAND MARK BELOW, and it was
 * found the hard way: this rule lived in app-shell.css, which only the signed-in layout loads,
 * while the field component renders icons on the login page. Every glyph there - the mail, the
 * lock and the show-password eye - was unsized and unstroked. The markup was correct, the sprite
 * was served, the symbols were present, and nothing was visible.
 *
 * An icon is a component primitive, not a shell part. The test for whether a rule belongs in
 * app-shell.css is whether the SHELL is the only thing that can render it.
 *
 * stroke-width is deliberately not set on the sprite's <symbol> elements. It is an inheritable
 * SVG presentation attribute, so setting it here lets it cross into the <use> shadow content and
 * keeps --icon-stroke-width the single place the value lives. Setting it on the symbol would
 * block that inheritance. Same reasoning for currentColor. That is also why the missing rule
 * removed the stroke entirely rather than merely the size. */
.sw-icon {
  inline-size: var(--icon-size-md);
  block-size: var(--icon-size-md);
  stroke-width: var(--icon-stroke-width);
  flex: none;
}

/* The inline sprite container - _IconSprite.cshtml, first element in <body> on every layout.
 *
 * NOT display:none, AND NOT THE `hidden` ATTRIBUTE. A display:none ancestor stops <use> rendering
 * the symbol it references, and that is not a theory: the sprite shipped as an external file whose
 * root <svg> carried `hidden`, and every icon in the product drew nothing while the file served
 * 200 with correct symbols, correct CSS and correct markup. Out of flow at zero size is the shape
 * that leaves symbols referenceable. */
.sw-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.sw-icon--sm { inline-size: var(--icon-size-sm); block-size: var(--icon-size-sm); }
.sw-icon--lg { inline-size: var(--icon-size-lg); block-size: var(--icon-size-lg); }


/* ===================================================================================
 * Brand mark - the real asset, derived. See ADR-0017.
 * ===================================================================================
 *
 * Here rather than in app-shell.css because the signed-out layout renders the lockup and does
 * not load the shell stylesheet.
 *
 * The intrinsic size is declared on the <img> so the box is reserved before the bytes arrive;
 * these rules only set the RENDERED size, and height: auto keeps the ratio honest.
 */

.sw-brand {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

/* THE FULL VARIANT IS NOW MARK-PLUS-TEXT, so the sizing lives on the mark and the word rather than
 * on a single image. The raster lockup is not rendered anywhere - see _Brand.cshtml. */
.sw-brand--full {
  gap: var(--space-2);
}

.sw-brand--full .sw-brand__mark {
  width: 32px;
  height: auto;
}

/* THE WORDMARK IS ARTWORK AGAIN AND IS AN INLINE <svg>, not a span of display type. It was text
 * because the only wordmark asset was a near-black raster that dark mode made unusable; it is now a
 * traced single path filled with currentColor, so it takes whatever colour it inherits.
 *
 * currentColor IS INHERITED RATHER THAN DECLARED, and that is the whole point: on the topbar and
 * the login card it resolves to --color-text-primary through the cascade, and on the navigation
 * panel to the panel's own foreground - one asset, both themes, both surfaces, no variant.
 *
 * SIZED BY HEIGHT WITH width:auto, WHICH IS THE OPPOSITE OF THE <img> BESIDE IT. The mark is
 * declared by width because it is square and its box has to be reserved before its bytes arrive;
 * this has no bytes to wait for - the symbol is in the same document - and a 3.56:1 lockup is
 * easier to reason about beside a 32px mark when the SHARED dimension is the one that is set.
 * The em value ties it to the type scale rather than to a pixel somebody chose.
 *
 * display:block, because an inline <svg> sits on the text baseline and leaves descender space
 * under it, which reads as the lockup hanging above its own centre line in a flex row. */
.sw-brand__wordmark {
  display: block;
  block-size: 1.25em;
  inline-size: auto;

  /* THE RATIO IS DECLARED BECAUSE AN <svg> WITH A viewBox AND NO width/height DOES NOT TAKE ITS
   * INTRINSIC RATIO FROM THAT viewBox — it takes the CSS replaced-element DEFAULT of 300x150.
   *
   * Measured before this line existed: the login card's wordmark box was exactly 300x28, the
   * sidebar's 179x20 and the topbar's 114x20 - the default width, shrunk by flex to whatever the
   * container allowed. The ARTWORK was correct in all three, because preserveAspectRatio letterboxes
   * rather than distorts, so it drew 71px wide and centred in a box up to four times that. The
   * defect was invisible as a wrong shape and visible as a wordmark drifting away from the mark
   * beside it.
   *
   * THESE ARE THE ASSET'S OWN viewBox NUMBERS, TYPED A SECOND TIME, AND THE COPY IS CHECKED RATHER
   * THAN DERIVED. CSS cannot read the SVG; generating a custom property would need a build step this
   * project does not have, and emitting one from Razor into a style attribute is refused outright by
   * ADR-0006's CSP. So BrandSpriteTests parses this declaration and compares it against
   * docs/brand/novofarlab-wordmark.svg. A derivation would make divergence impossible; this makes
   * divergence go red at the moment it happens, which is the same practical guarantee.
   *
   * IT WAS 1238 / 348 UNTIL 2026-09-07, and the change is the crop rather than a new asset. */
  aspect-ratio: 1238 / 249;

  /* flex:none so the box is never shrunk below that. Without it a narrow topbar squeezes the item
   * and the letterboxing returns, silently and only at some widths. */
  flex: none;

  color: var(--color-text-primary);
}

/* The NF diamond, square, cropped from the same file. The sidebar's variant, and the only one
 * that works there - it also solves the 72px collapsed state, which a 4.6:1 lockup never could. */
.sw-brand--monogram .sw-brand__mark,
.sw-brand--monogram img {
  width: 40px;
  height: auto;
}

/* On the login page the mark sits above the card, larger than in the topbar but not dominant - it
 * is a sign-in, not a landing page, so the card stays the largest object on screen. */
.sw-auth__brand .sw-brand--full .sw-brand__mark {
  width: 48px;
  height: auto;
}

/* THE LOGIN CARD DROPPED FROM 1.75em TO THE BASE 1.25em ON 2026-09-07, WHICH IS A REDUCTION THAT
 * KEEPS IT THE SAME SIZE. The crop took 29% of dead space out of the asset's box, so every
 * placement drew 40% larger artwork at an unchanged block-size. Measured: the login wordmark's ink
 * went from 19.9px to 27.8px without anybody asking it to. Removing the override puts it back at
 * 19.9px against the same 48px mark - optically identical to what shipped, which is what this card
 * wanted, because nobody complained about it.
 *
 * The rule is deleted rather than set to 1.25em: an override that restates the base is a value
 * somebody has to keep in step for no reason. */

/* ===================================================================================
 * Buttons — §8.1
 * =================================================================================== */

.sw-button {
  /* 32 / 40 / 48, from §8.1. Heights are component tokens because the design system specifies
   * control heights rather than deriving them from the spacing scale. */
  --sw-button-height: 40px;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);

  min-height: var(--sw-button-height);
  padding-inline: var(--space-4);

  border: 1px solid transparent;
  border-radius: var(--radius-md);

  font-family: inherit;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;

  cursor: pointer;

  /* Named properties rather than `all`: transitioning everything animates layout changes too,
   * which is how a control ends up sliding when its text wraps.
   *
   * TRANSFORM JOINED THE LIST IN V2, and it is what makes the press read as a press. Colour and
   * shadow alone change how a control LOOKS; a single pixel of downward travel changes what it
   * feels like, and it is the cheapest premium cue there is. §9 rule 5's block suppresses the
   * duration of the whole shorthand under prefers-reduced-motion, transform included. */
  transition:
    background-color var(--duration-fast) var(--easing-standard),
    border-color var(--duration-fast) var(--easing-standard),
    color var(--duration-fast) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard),
    transform var(--duration-fast) var(--easing-standard);
}

/* One pixel, on every variant, filled or not. More than that reads as a toy. */
.sw-button:active:not(:disabled) {
  transform: translateY(1px);
}

.sw-button--sm {
  --sw-button-height: 32px;

  padding-inline: var(--space-3);
  font-size: var(--font-size-xs);
}

.sw-button--lg {
  --sw-button-height: 48px;

  padding-inline: var(--space-6);
  font-size: var(--font-size-base);
}

/* §8.1: minimum hit area 44x44 below the lg breakpoint. The height grows; the font does not, so
 * a small button on a phone is a bigger target rather than a bigger-looking button. */
@media (max-width: 1023px) {
  .sw-button {
    min-height: max(var(--sw-button-height), 44px);
  }
}

.sw-button--block {
  display: flex;
  width: 100%;
}

/* ---------- Primary ---------- */

.sw-button--primary {
  background: var(--color-action-primary);
  color: var(--color-text-on-action);
  box-shadow: var(--shadow-sm);
}

.sw-button--primary:hover:not(:disabled) {
  background: var(--color-action-primary-hover);
  box-shadow: var(--shadow-md);
}

.sw-button--primary:active:not(:disabled) {
  background: var(--color-action-primary-active);

  /* Elevation drops on press, so the control reads as going down rather than merely changing
   * colour. It is the cheapest possible tactile cue and it costs one line. */
  box-shadow: none;
}

/* ---------- Secondary ---------- */

.sw-button--secondary {
  background: transparent;
  border-color: var(--color-action-secondary-border);

  /* THE LABEL TAKES THE TEXT TOKEN AND THE BORDER TAKES THE FILL ONE - see .sw-chip below for the
   * full reasoning. Not caught by the sweep, and that is the reason it is worth a comment: a
   * transparent button inherits whatever surface it is dropped on, and --color-action-primary
   * clears 4.5:1 on the panel (5.24:1) while failing on the hover surface (4.00:1). Every secondary
   * button in the audited set happens to sit on a panel. The rule was already broken; nothing had
   * put one in a table row yet. */
  color: var(--color-tone-brand-fg);
}

.sw-button--secondary:hover:not(:disabled) {
  background: var(--color-action-secondary-hover);
}

/* §11 ITEM 15 IS CLOSED HERE. This used to be --color-surface-selected, which resolves to the same
 * --color-primary-50 as the hover above it - so pressing a secondary button produced no change from
 * hovering it, and §8.1's table asked for a step darker with no alias to reach it. V2 names that
 * step: --color-action-secondary-active. */
.sw-button--secondary:active:not(:disabled) {
  background: var(--color-action-secondary-active);
}

/* ---------- Tertiary / link ---------- */

.sw-button--tertiary {
  background: transparent;
  padding-inline: var(--space-2);
  color: var(--color-text-link);
}

.sw-button--tertiary:hover:not(:disabled),
.sw-button--tertiary:active:not(:disabled) {
  text-decoration: underline;
}

/* ---------- Destructive ---------- */

.sw-button--destructive {
  background: var(--color-action-destructive);
  color: var(--color-text-on-action);
  box-shadow: var(--shadow-sm);
}

.sw-button--destructive:hover:not(:disabled) {
  background: var(--color-action-destructive-hover);
  box-shadow: var(--shadow-md);
}

.sw-button--destructive:active:not(:disabled) {
  box-shadow: none;
}

/* ---------- Disabled ---------- */

/* §8.1: disabled uses the `disabled` attribute AND aria-disabled, never opacity alone. Opacity
 * dims the text along with everything else and lands the contrast wherever it lands; explicit
 * disabled tokens keep it a measured value. */
.sw-button:disabled,
.sw-button[aria-disabled="true"] {
  background: var(--color-action-disabled-bg);
  border-color: transparent;
  color: var(--color-action-disabled-text);
  box-shadow: none;
  cursor: not-allowed;
}

.sw-button--secondary:disabled,
.sw-button--tertiary:disabled {
  background: transparent;
  border-color: var(--color-action-disabled-text);
  color: var(--color-action-disabled-text);
}

/* ---------- Loading ---------- */

/* §8.1: the spinner replaces the icon slot, the LABEL PERSISTS, and the control is aria-busy.
 * A button whose text disappears while it works is a button that has changed width mid-click. */
.sw-button__spinner {
  /* LAYER 3, AND A GAP RATHER THAN A PREFERENCE. §8.1 requires a loading state and specifies no
   * spinner: no diameter, no stroke, no speed. The tokens stop at --duration-base (200ms), which
   * is a transition duration - a spinner rotating five times a second reads as panic.
   *
   * 700ms is roughly one turn per beat at a walking pulse, which is the convention every mature
   * component library lands near. It is declared here with its reasoning rather than typed into
   * the animation, and raised as design-system.md §11 item 9 so the eventual answer replaces a
   * decision instead of filling a blank. */
  --sw-spinner-duration: 700ms;

  width: 1em;
  height: 1em;
  flex: none;

  border: 2px solid currentcolor;
  border-block-start-color: transparent;
  border-radius: var(--radius-full);

  animation: sw-spin var(--sw-spinner-duration) linear infinite;
}

@keyframes sw-spin {
  to {
    transform: rotate(1turn);
  }
}

/* ===================================================================================
 * Form fields — §8.2
 * =================================================================================== */

.sw-field {
  display: flex;
  flex-direction: column;

  /* TIGHT WITHIN THE GROUP. Label and input belong to each other; the gap between fields is
   * --space-6, three times this. Uniform spacing is what makes a form look like a spreadsheet. */
  gap: var(--space-2);
}

/* Field name on the left, "Requerido" at the end of the same line. A grey parenthetical after the
 * name reads as a footnote; a right-aligned marker reads as a column of metadata, which is what
 * it is. */
.sw-field__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-primary);
}

/* §8.2: required fields are marked in the LABEL TEXT, not by colour or an asterisk alone - so
 * this span stays INSIDE <label for>. Moving it to a sibling would take it out of the accessible
 * name, and the field would announce as "Correo electrónico" with the requirement lost. */
.sw-field__required {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-regular);
  color: var(--color-text-secondary);
}

/* ---------- The control row: icon, input, optional trailing button ---------- */

.sw-field__row {
  position: relative;
  display: flex;
  align-items: center;
}

/* Decorative. The visible <label> is the accessible name; §9 rule 3 - colour and iconography are
 * reinforcement, never the only carrier. */
.sw-field__icon {
  position: absolute;
  inset-inline-start: var(--space-4);
  color: var(--color-text-secondary);
  pointer-events: none;
}

.sw-field__control {
  width: 100%;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);

  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-primary);

  color: var(--color-text-primary);
  font-family: inherit;

  /* 16px, AND IT IS NOT NEGOTIABLE. Below 16px iOS Safari zooms the viewport on focus, which
   * reflows the page under the user's hands mid-entry and does not zoom back out. */
  font-size: var(--font-size-base);

  transition:
    border-color var(--duration-fast) var(--easing-standard),
    box-shadow var(--duration-fast) var(--easing-standard);
}

/* Room for the leading icon. --space-4 of gutter, the icon itself, then --space-3 before the
 * text - so entered text never runs under the glyph, at any zoom, because the reservation is
 * expressed in the same units the icon is. */
.sw-field__row--with-icon .sw-field__control {
  padding-inline-start: calc(var(--space-4) + var(--icon-size-md) + var(--space-3));
}

/* And room for the trailing toggle, sized from the touch target rather than from the glyph. */
.sw-field__row--with-action .sw-field__control {
  padding-inline-end: calc(var(--layout-touch-target-min) + var(--space-2));
}

.sw-field__control::placeholder {
  color: var(--color-text-secondary);
}

/* ---------- Show-password toggle ----------
 *
 * A SIBLING OF THE INPUT, never inside it, so the input's focus ring is not drawn around the
 * button and the button carries its own. It is hidden in markup and revealed by the script: a
 * reveal control that does nothing when JavaScript fails is worse than no control at all.
 */
.sw-field__action {
  position: absolute;
  inset-inline-end: 0;

  display: inline-flex;
  align-items: center;
  justify-content: center;

  /* §9 rule 6 - 44x44 minimum below lg. Applied unconditionally: the glyph is 20px and there is
   * no reason to make the target smaller on a desktop either. */
  inline-size: var(--layout-touch-target-min);
  block-size: var(--layout-touch-target-min);

  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.sw-field__action:hover {
  color: var(--color-text-primary);
}

/* §8.2 focus: border colour change PLUS the focus ring from §9. The ring comes from base.css's
 * :focus-visible; the border change alone would not be a focus indicator. */
.sw-field__control:focus {
  border-color: var(--color-border-focus);
}

.sw-field__control[aria-invalid="true"] {
  border-color: var(--color-status-error-fg);
}

.sw-field__control:disabled {
  border-color: var(--color-border-subtle);
  background: var(--color-surface-secondary);
  color: var(--color-text-disabled);
  cursor: not-allowed;
}

.sw-field__hint {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

/* A MONEY INPUT'S ECHO — the figure being typed, as the product writes money (money-echo.js, 2026-09-15). The field's
 * own weight rather than a hint's, because it is the value read back; tabular figures, so the digits do not shift
 * sideways as they are typed. Created by the script, so it takes no room without one. */
.sw-field__echo {
  margin: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-primary);
}

.sw-field__error {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-status-error-fg);
}

/* THE ELEMENT IS NOW RENDERED ON EVERY VALIDATED FIELD, EMPTY, so the client validator has
 * somewhere to write and aria-describedby can point at it before there is anything to say.
 * Empty, it must occupy nothing: a blank line under every input is a layout that shifts the
 * moment a message arrives. */
.sw-field__error:empty {
  display: none;
}

/* THERE ARE NO --invalid/--valid MODIFIERS HERE, AND THAT IS A CORRECTION. They were added on the
 * assumption that $.validator.setDefaults could name the classes jQuery Validate toggles; it
 * cannot, because jquery.validate.unobtrusive sets errorClass in the options it passes to
 * .validate(), and those win over the defaults. The rules would have styled classes nothing ever
 * applies - CSS for a state that cannot occur, which reads as coverage.
 *
 * WHAT THE LIBRARY ADDS INSTEAD IS field-validation-error, AND IT HAS NO RULE HERE ON PURPOSE.
 * It lands on the element above, which .sw-field__error already styles; measured in Chrome after a
 * client-side refusal - display=block, color=rgb(180, 35, 24), fontSize=14px, all inherited from
 * our own class. Adding a rule for the library's name would be a second place to change the error
 * colour.
 *
 * NOTE FOR §10 RULE 10: the class-coverage theory reads SERVED HTML, so it cannot see this class
 * at all - JavaScript adds it after load. A class applied at runtime is outside what that theory
 * can check, and the computed style above is how it was checked instead. :empty is what governs
 * visibility either way: the message is present or it is not. */

/* ---------- Autofill ----------
 *
 * CHROME PAINTS AUTOFILLED FIELDS A PALE BLUE-YELLOW AND background-color CANNOT OVERRIDE IT.
 * MDN documents why: the user-agent stylesheet declares it !important on an internal selector.
 *
 *   input:-internal-autofill-selected {
 *     background-image: none !important;
 *     background-color: light-dark(rgb(232, 240, 254), rgba(70, 90, 126, 0.4)) !important;
 *     color: fieldtext !important;
 *   }
 *
 * "you cannot override the default background-color, background-image, or color values on
 * selected autofilled input elements in your own rules"
 * https://developer.mozilla.org/en-US/docs/Web/CSS/:autofill
 *
 * So the background is painted with a very large INSET box-shadow, which that rule does not
 * cover, and the text colour with -webkit-text-fill-color, which is not `color`.
 *
 * THE FOCUS RING SURVIVES, AND THAT WAS THE THING TO GET RIGHT. §9 rule 1 draws focus with
 * `outline`, which is painted outside the border box; an inset box-shadow is painted inside the
 * padding box. They cannot overlap, so the ring is not covered. Had the focus indicator been a
 * box-shadow, this rule would have replaced it outright - box-shadow is one property, not a
 * stack - and the most important accessibility affordance on the page would have vanished on
 * exactly the fields a returning user lands in. Asserted by ComponentStylesheetTests.
 *
 * :autofill first, -webkit-autofill second: the standard selector is not Baseline, and a browser
 * that understands neither is left with a yellow field rather than an unstyled one.
 */
.sw-field__control:autofill,
.sw-field__control:-webkit-autofill {
  box-shadow: inset 0 0 0 100px var(--color-surface-primary);
  -webkit-text-fill-color: var(--color-text-primary);
}

.sw-field__control:autofill:focus,
.sw-field__control:-webkit-autofill:focus {
  /* Repeated because the shadow is re-declared on focus in some engines. The outline is
   * untouched either way; this only keeps the background painted. */
  box-shadow: inset 0 0 0 100px var(--color-surface-primary);
}

/* ===================================================================================
 * Checkbox — §8.10
 * =================================================================================== */

.sw-checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-3);

  /* The whole row is the target, not the 16px box. Below lg this must clear 44px (§9 rule 6). */
  min-height: 44px;

  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
  cursor: pointer;
}

.sw-checkbox__control {
  width: 20px;
  height: 20px;
  flex: none;

  /* accent-color tints the native control, so the checked state is branded without rebuilding
   * the widget out of a div - which is how checkboxes lose their keyboard behaviour. */
  accent-color: var(--color-action-primary);
  cursor: pointer;
}

/* ---- A group of checkboxes posting to one field ----
 *
 * A FIELDSET WITH A LEGEND, NOT A DIV WITH A HEADING ABOVE IT. The legend is appended to every
 * box's accessible name, so a list announces as "Listas de precios, Lista 3" rather than as a bare
 * number with no scope. A <p> over a <div> looks identical on screen and carries none of that.
 *
 * The user agent's border and padding are removed rather than restyled: the group sits inside a
 * dialog that already draws the frame, and a second box around it reads as a nested panel.
 */
/* NO min-inline-size RESET HERE, AND THAT IS A MEASUREMENT RATHER THAN AN OMISSION. A fieldset's
 * default minimum inline size is min-content, which is the classic reason one refuses to shrink -
 * so this rule carried `min-inline-size: 0` with a comment saying it was what stopped the sheet
 * scrolling sideways at 320px. Removing it changed nothing: the option grid's min-content is one
 * 12rem track, comfortably inside the ~288px a 320px sheet offers, so the default never bites.
 *
 * The property it was meant to protect is now covered by a test that can fail - the export dialog
 * body is asserted not to scroll sideways at 320px, and widening the track to 24rem turns it red.
 * That is worth more than a defensive declaration nobody re-checks, standing on a reason this
 * layout cannot produce.
 */
.sw-fieldset {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  margin: 0;
  padding: 0;
  border: 0;
}

.sw-fieldset__legend {
  /* The user agent insets a legend from a border box this no longer has. */
  padding: 0;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-primary);
}

/* What an empty selection means. The same treatment as .sw-field__hint, which is the same kind of
 * sentence in the same position under a label. */
.sw-fieldset__note {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

/* TWO COLUMNS WHERE THERE IS ROOM AND ONE WHERE THERE IS NOT, decided by the CONTAINER rather than
 * the viewport - this group renders inside a dialog, whose width is the dialog's and not the
 * screen's. auto-fit collapses the track count itself, so no breakpoint and no container query.
 *
 * 12rem is the width at which a checkbox row holding "Categoría de aseo personal" stops wrapping;
 * measured rather than chosen, and stated because it is a raw value in a file of tokens.
 */
.sw-fieldset__options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 0 var(--space-4);
}

/* ---- A group that IS the content, rather than one field among many ----
 *
 * §8.10.1 REMOVES THE USER AGENT'S FIELDSET BOX BECAUSE "the group sits inside a frame something
 * else already draws" — true where a group is one control on a form beside fields and buttons.
 * It is not true when the groups ARE the dialog: two bare stacks of checkboxes under two bare
 * legends read as one undifferentiated list with a word in the middle of it, and a reader ticking
 * boxes has to work out where one question ends and the next begins.
 *
 * SO THE BOX COMES BACK AS AN OPT-IN MODIFIER rather than as a change to the default. The failure
 * of the default in a selection dialog is legibility; the failure of a box around every group on
 * every form is a page of nested panels.
 *
 * NOTHING SEMANTIC MOVES. It is still a <fieldset> with a <legend>, the legend is still part of
 * every box's accessible name, and the note is still where "sin marcar ninguna = todas" is said.
 * §8.10.1 is unchanged; this is chrome. */
.sw-fieldset--panel {
  /* --space-3 RATHER THAN --space-4, TIGHTENED 2026-08-19 WHEN THE FORMAT CONTROL ARRIVED. The
   * group already sits inside a dialog body that pads by --space-6, so this only has to separate
   * the box from its neighbour rather than from the frame — and a panel's own padding is the one
   * place in a stacked form where a step is invisible to a reader and worth 16px of a fixed budget.
   *
   * IT IS THE EXPORT DIALOG'S VERTICAL BUDGET THAT FORCED IT, AND THAT BUDGET IS NOW TIGHT: at
   * 1366x768 the body has a few pixels of headroom with two groups and the format control. The next
   * control added to that dialog will not fit, and the answer then is a shorter option list or a
   * taller minimum viewport rather than another step off this. Recorded so the next person meets a
   * measurement instead of a surprise. ADR-0045. */
  padding: var(--space-3);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-secondary);
}

.sw-fieldset--panel > .sw-fieldset__legend {
  /* THE LEGEND SITS ON THE BORDER BY DEFAULT and a fieldset's legend is not an ordinary child: the
   * user agent takes it out of flow and cuts a gap in the top edge for it. `float: none` is not
   * enough and `display: block` is what actually returns it to the box, which is the documented way
   * to make a legend behave like a heading inside its own panel.
   * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/legend */
  display: block;
  inline-size: 100%;
  margin-block-end: var(--space-1);
}

/* ===================================================================================
 * Validation summary — §8.2
 * =================================================================================== */

/* IT TAKES FOCUS AND IT IS NOT role="alert". THE TWO ARE MUTUALLY EXCLUSIVE.
 *
 * The W3C ARIA Authoring Practices Guide is explicit about the alert role: "Because alerts are
 * intended to provide important and potentially time-sensitive information without interfering
 * with the user's ability to continue working, it is crucial they do not affect keyboard focus."
 * https://www.w3.org/WAI/ARIA/apg/patterns/alert/
 *
 * design-system.md §8.2 requires the summary to take focus on submit failure. Both cannot hold,
 * and the conflict is resolved in favour of the focus move: the APG's reason for the rule is that
 * an alert arrives UNSOLICITED, and a validation summary arrives because the user pressed submit
 * one moment earlier and is waiting for the answer.
 *
 * Moving focus is also what announces it - a screen reader reads the newly focused element - so
 * role="alert" on top would announce the same content a second time as a live region. The field
 * errors are separately announced through aria-describedby when the user reaches each field, so
 * a third announcement is the one thing this must not add.
 *
 * Recorded in design-system.md §8.2, because "no role here" looks like an omission.
 */
.sw-validation-summary {
  padding: var(--space-4);

  border: 1px solid var(--color-status-error-fg);
  border-radius: var(--radius-md);
  background: var(--color-status-error-bg);

  color: var(--color-status-error-fg);
  font-size: var(--font-size-sm);
}

.sw-validation-summary__title {
  margin-block-end: var(--space-2);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
}

.sw-validation-summary ul {
  margin: 0;
  padding-inline-start: var(--space-5);
}

/* The framework's tag helper emits the list even with nothing in it, and an empty red panel on a
 * page nobody has submitted is worse than no panel. */
.sw-validation-summary:not(.sw-validation-summary--visible) {
  display: none;
}

/* ===================================================================================
 * Alert — §8.9. Four tones; pending and inactive are badge states, not alert tones (§11 item 14).
 * =================================================================================== */

.sw-alert {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4);

  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
}

.sw-alert__icon {
  flex: none;
  width: 20px;
  height: 20px;
}

.sw-alert--success {
  border-color: var(--color-status-success-fg);
  background: var(--color-status-success-bg);
  color: var(--color-status-success-fg);
}

.sw-alert--info {
  border-color: var(--color-status-info-fg);
  background: var(--color-status-info-bg);
  color: var(--color-status-info-fg);
}

.sw-alert--warning {
  border-color: var(--color-status-warning-fg);
  background: var(--color-status-warning-bg);
  color: var(--color-status-warning-fg);
}

.sw-alert--error {
  border-color: var(--color-status-error-fg);
  background: var(--color-status-error-bg);
  color: var(--color-status-error-fg);
}

/* ===================================================================================
 * Status badges — §8.4
 * =================================================================================== */

/* A SOLID PILL SINCE V2, AND THIS IS THE FIX FOR §11 ITEM 20 / OPEN QUESTION 64.
 *
 * It used to be the soft fg/bg pair: --color-success-50 behind --color-success-600. That tint
 * measures about 1.06:1 against a white panel - the same order §9 records for the page background
 * against a card and describes as "not a separation, it is two whites". So the pill was invisible
 * and what a reader actually saw was a coloured word. The surface was REPORTED as rendering status
 * "as plain text, Activa in green", which was a correct description of a correctly built component.
 *
 * Filled, with --color-text-on-status on top. A status has to be findable at a glance down a column
 * of forty rows, and a shape is findable in a way a word is not.
 *
 * ALERTS KEEP THE SOFT PAIR and that is deliberate: an alert is a paragraph, and a solid red
 * paragraph is a different and much heavier component. §8.9 and §8.4 want different things from the
 * same tone, which is why v2 gives each tone a `solid` role beside its fg/bg one.
 *
 * The letter-spacing is the small-text rule from §5's new --letter-spacing-wide: 12px of dense text
 * inside a tight pill closes up, and opening it is most of what makes the pill look set. */
.sw-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);

  padding: 3px var(--space-3);

  border-radius: var(--radius-full);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--letter-spacing-wide);
  line-height: var(--line-height-tight);

  color: var(--color-text-on-status);

  /* §8.4 and §9 rule 3: the badge ALWAYS carries text. Colour is reinforcement, never the sole
   * carrier of meaning. There is no icon-only variant on purpose. */
  white-space: nowrap;
}

.sw-badge--success  { background: var(--color-status-success-solid); }
.sw-badge--info     { background: var(--color-status-info-solid); }
.sw-badge--warning  { background: var(--color-status-warning-solid); }
.sw-badge--error    { background: var(--color-status-error-solid); }
.sw-badge--pending  { background: var(--color-status-pending-solid); }
.sw-badge--inactive { background: var(--color-status-inactive-solid); }

/* ===================================================================================
 * CONTENT-PAGE COMPOSITION: the page header, the panel, the toolbar, the form column.
 *
 * §8 DEFINES COMPONENTS AND NOT HOW A PAGE ARRANGES THEM, so this block is a layout decision
 * rather than a component set - ADR-0025, which is where the argument lives. Four rules, each
 * answering a question §8 leaves open:
 *
 *   .sw-page-header  what a content page says about itself, and where its primary action sits
 *   .sw-panel        what groups a set of components into one object on the page background
 *   .sw-toolbar      §8.3's "filter bar" as a SURFACE, which §8.3 names and does not draw
 *   .sw-form-column  the single readable column a form is constrained to
 *
 * THIS BLOCK REPLACES `.sw-card`, WHICH HAD A RULE AND NO CALLER. It declared exactly the four
 * values this panel does - surface-primary, radius-lg, shadow-sm, space-6 - under a heading about
 * the signed-out frame, and nothing in the application ever rendered it. Renaming rather than
 * adding is deliberate: a `.sw-panel` beside a dead `.sw-card` is two names for one object, which
 * is the class-level version of the near-duplicate token §1 exists to prevent.
 *
 * components.css RATHER THAN app-shell.css, per §10 rule 10: app-shell.css is only for what the
 * signed-in shell is the only thing that can render, and nothing about a panel or a form column is
 * shell-specific. The existing listing furniture (.sw-filters, .sw-pagination, .sw-role-list) sits
 * in app-shell.css and is left there - moving it is a change with no reader-visible effect, and
 * this file's boundary rule is the safe direction to err in.
 * =================================================================================== */

/* ===================================================================================
 * The gallery's two-theme comparison.
 *
 * A REVIEW AFFORDANCE, AND THE ONLY PLACE IN THE PRODUCT WHERE TWO THEMES ARE ON ONE SCREEN. Each
 * panel carries data-theme, which sets color-scheme for its subtree, which is what light-dark()
 * reads - so the components inside are the SAME partials resolving different halves of the same
 * tokens. Nothing here is a second set of styles, and there is nothing to keep in step.
 *
 * The panels paint --color-surface-page rather than being transparent, because a dark theme on a
 * light page background is not what a dark theme looks like.
 * =================================================================================== */

.sw-gallery__themes {
  display: grid;
  gap: var(--space-4);
  margin-block-end: var(--space-8);
}

@media (min-width: 768px) {
  .sw-gallery__themes {
    grid-template-columns: 1fr 1fr;
  }
}

.sw-gallery__theme {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);

  padding: var(--space-6);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  background: var(--color-surface-page);
  color: var(--color-text-primary);
}

.sw-gallery__theme-caption {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
}

/* ---- The page ----
 *
 * ONE COLUMN WITH ONE GAP, and it exists because the alternative is every block owning its own
 * bottom margin. The listing before this pass had none at all: the header, the banner, the filters
 * and the table sat flush against one another with only the components' internal spacing between
 * them, which is most of what "floating on the page background" looked like.
 *
 * gap rather than margin, so the FIRST child has no space above it and the LAST none below - the
 * two cases a margin-based rhythm always has to special-case afterwards. */
.sw-page {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* ---- The page header ---- */

.sw-page-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.sw-page-header__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* §5's page-title row: 2xl-3xl, 600-700, DISPLAY face, and the one place --letter-spacing-tight
 * is permitted - display type at --font-size-xl and above. */
.sw-page-header__title {
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);
  letter-spacing: var(--letter-spacing-tight);
  color: var(--color-text-primary);
}

/* THE SAME MEASURE THE FORM COLUMN USES, and that is the token's meaning rather than a reuse of
 * convenience: --layout-form-max-width is "the widest a single readable column may get", and a
 * lede running the full 1440px is the same defect as a 1400px email input in a different medium.
 * ≈64 characters at --font-size-base, inside the 45-75 a line of text is comfortable at. */
.sw-page-header__lede {
  max-inline-size: var(--layout-form-max-width);
  color: var(--color-text-secondary);
}

/* A row rather than a column even on a phone: the actions are one or two short controls, and
 * stacking a full-width button under a lede pushes the content the reader came for below the
 * fold. flex-wrap is what keeps that honest at 320px. */
.sw-page-header__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* ---- The panel ---- */

.sw-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--color-surface-primary);
  box-shadow: var(--shadow-sm);
}

/* §5's section-heading row: xl, 600, UI face. NOT the display face - that is reserved for page
 * titles and KPI figures so it stays a deliberate accent (§5). */
.sw-panel__title {
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-tight);
  color: var(--color-text-primary);
}

.sw-panel__intro {
  max-inline-size: var(--layout-form-max-width);
  color: var(--color-text-secondary);
}

/* TWO CLASSES THAT HAVE BEEN RENDERED WITH NO RULE BEHIND THEM SINCE THEY WERE WRITTEN.
 *
 * FOUND BY A TEST, NOT BY READING — `Every_class_the_page_renders_has_a_rule`, added in Pass P-ii-x
 * and the first thing on this repository ever to compare a SIGNED-IN page's classes against the
 * stylesheet it loads. `ServedIdentityPagesTests` has done it for the signed-out frame since that
 * frame was built; nothing did it for anything behind a login, so a class typed into a view with
 * nothing behind it rendered as a plain element and failed no assertion.
 *
 * BOTH ARE ON TWO SURFACES — the user detail's salesperson panel and the order detail's note — so
 * this rule changes the appearance of a page outside the pass that added it. That is the correction
 * rather than a side effect of it: the markup has been asking for a de-emphasised run of text and
 * getting body text, on both.
 *
 * THE BLOCK THEY NAME IS NOT THE BLOCK THEY BELONG TO, and that is left alone deliberately.
 * `.sw-detail` above is the customer detail's CONTAINER, and `.sw-detail__value` is not an element
 * of it — the name was chosen by analogy. Renaming is a change to two view files for no reader's
 * benefit, and it is worth more written down here than done in a commit about dialogs. */
.sw-detail__value {
  color: var(--color-text-primary);
}

.sw-detail__muted {
  color: var(--color-text-secondary);
}

/* THE PANEL'S OWN ACTION ROW — Pass P-ii-x, X-1.
 *
 * WHY IT IS NOT .sw-toolbar. The toolbar is §8.3's filter surface: it paints
 * --color-surface-secondary and carries its own padding, which inside a panel is a grey slab in a
 * white box - measured and corrected in X-3 for the listing's filter row. This is a row of controls
 * belonging to the panel they sit in, with no surface of its own.
 *
 * IT SETS NO MARGIN. .sw-panel is a flex column with gap: var(--space-4), so the space above this
 * row is the panel's, and a margin here would add to it rather than replace it - the same trap the
 * .sw-panel .sw-table rule above records.
 *
 * flex-wrap, because a panel may offer more than one action and a phone is 320px wide. Nothing here
 * offers two yet; the alternative is a rule that has to be revisited the first time one does. */
.sw-panel__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

/* Same reasoning as the toolbar's chip row below: --space-6 under the table is for a table floating
 * on a page. Inside a panel the column gap owns the spacing, and the two together leave a band of
 * empty surface between the last row and the panel's own padding - which reads as the table having
 * stopped early rather than as breathing room. Found by reading the composed page, which is the only
 * instrument that could have. */
.sw-panel .sw-table {
  margin-block-end: 0;
}

/* The panel is a fragment target - the page header's primary action points at it - so it takes
 * focus, and a focused panel must show it. tabindex="-1" alone scrolls without telling a keyboard
 * user anything arrived. :focus-visible rather than :focus so a mouse click on the anchor does not
 * ring the whole panel. */
.sw-panel:focus-visible {
  outline: 2px solid var(--color-border-focus);
  outline-offset: 2px;
}

/* ---- The toolbar: §8.3's filter bar, as a surface ----
 *
 * §8.3 specifies the applied-filter CHIPS in full and says nothing about what the controls sit on;
 * §3 answers it anyway - --color-surface-secondary is annotated "table headers, filter bars". So
 * this is the token layer's own answer being drawn rather than a colour anybody chose. */
.sw-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-surface-secondary);
}

/* The chip row's own bottom margin is for a chip row floating on a page. Inside the toolbar the
 * gap above owns the spacing, and both would double it. */
.sw-toolbar .sw-filter-bar {
  margin-block-end: 0;
}

/* ---- The form column ----
 *
 * ONE CLASS, ONE DECLARATION, AND IT IS THE POINT OF THE TOKEN. Every form that is a column of
 * controls carries this; nothing writes a width. See --layout-form-max-width in tokens.css for why
 * the value is what it is, and §11 item 18 for its sign-off. */
.sw-form-column {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);

  max-inline-size: var(--layout-form-max-width);
}

/* ---- The panel grid: the SIXTH class, and it is named rather than added quietly ----
 *
 * ADR-0025 gave content pages five classes and every one of them was derived from a single-column
 * list page. A dashboard is not a column, and stacking four panels down a 1440px screen is not a
 * dashboard - so the five did not compose, and this is the honest report of that rather than a
 * quiet addition. ADR-0035 carries the argument.
 *
 * ONE STEP, NOT THREE. .sw-kpi-grid goes 1 / 2 / 4 because a KPI card is a figure and four fit on a
 * row; a panel holds a heading and a body, and a third or fourth column makes each one too narrow to
 * be worth having. So: stacked, then 2-up at md, and nothing after that.
 *
 * NO NEW TOKEN. --space-6 is the gap .sw-page already puts between its own blocks, so a grid of
 * panels sits on the same rhythm as a column of them.
 *
 * AND "2-up at md" IS NOW "2-up at 40rem OF ITS OWN CONTAINER" - see the container query below. The
 * argument above is unchanged; only what it measures moved. */
.sw-panel-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
}

/* ---- The detail's own layout context ----
 *
 * THE SAME PARTIAL RENDERS ON A 1440px PAGE AND INSIDE A 32rem DIALOG, and until 2026-08-14 both
 * asked the VIEWPORT how much room they had. In the dialog that answer is wrong by an order of
 * magnitude: the panels went two-up inside 28rem and the third was orphaned beside dead space, the
 * label/value grid gave each value a ~4rem column, and the synchronised timestamp broke at every
 * space it had - "12/08/2026" / "4:02" / "p." / "m.", four lines for one moment.
 *
 * A CONTAINER QUERY IS THE MECHANISM RATHER THAN A SECOND PARTIAL OR A FLAG. The markup is correct
 * in both places and the layout is what differs, so the layout is what should be conditional. A
 * dialog-only Razor branch would be a second rendering path in the file whose entire purpose is
 * that there is not one.
 *
 * `inline-size` rather than `size`: only the inline axis is queried, so the box still grows to fit
 * its content vertically. Baseline widely available since February 2023 - Chrome 105, Safari 16,
 * Firefox 110 - and it needs no script, which is what makes it CSP-safe by construction.
 * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries */
.sw-detail {
  container-type: inline-size;
  container-name: sw-detail;
}

/* TWO THRESHOLDS, AND THEY ARE NOT THE SAME NUMBER because they are not the same question.
 *
 * The panels go side by side only when two of them are still readable - 40rem is two ~20rem columns
 * plus the gap. The label/value grid needs less: 34rem is a 12rem label beside a value with 20rem
 * left, which is where the longest thing on this page, a grouped date and time, stops being at risk.
 *
 * THIS PARAGRAPH USED TO SAY BOTH SIT ABOVE THE DIALOG'S OWN WIDTH ON PURPOSE, and that stopped
 * being true on 2026-08-17. It was accurate while every dialog was capped at
 * --layout-form-max-width: the content box was about 28rem, neither query matched, panels stacked
 * and every value took the whole column.
 *
 * A READ-ONLY DIALOG IS NOW WIDER — `.sw-dialog--reading`, 56rem — and its content box is about
 * 40rem, which crosses BOTH thresholds. That is the intended effect and not a side effect: the
 * width was chosen to cross the 40rem line this rule had already drawn, so the two-column quick
 * view is this container query firing rather than a new layout. A WRITE dialog is unchanged and
 * still matches neither.
 *
 * The numbers above did not move. What changed is which containers are wide enough to reach them,
 * which is the whole argument for keying this on the container rather than on the viewport. */
@container sw-detail (min-width: 40rem) {
  .sw-panel-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* THE FICHA TAKES BOTH COLUMNS. It is prose in a label/value grid, and half a two-column dialog
   * gives it a ~20rem measure that breaks a paragraph over a dozen short lines. The panels that
   * pair well side by side are the ones made of short values. */
  .sw-panel--wide {
    grid-column: 1 / -1;
  }

  /* THE HERO GOES TWO-COLUMN AT THE SAME THRESHOLD THE PANELS DO, and sharing the query is the
   * decision rather than a convenience: one number decides whether this partial is "wide" and every
   * region answers it together. Two thresholds would give a container width at which the hero has
   * split and the panels have not, which reads as a rendering fault.
   *
   * The image column is fixed and the identity takes the rest - a hero whose photograph grew with
   * the container would put a 30rem product picture beside two lines of text. */
  .sw-hero {
    grid-template-columns: 16rem minmax(0, 1fr);
    align-items: start;
  }

  /* THE MONOGRAM HERO TAKES ITS OWN COLUMN WIDTH, and `auto` rather than a second literal: the
   * circle is 4rem and a 16rem column would leave three quarters of it empty beside a name that had
   * nowhere to go. The product's column is 16rem because a photograph fills it.
   *
   * IT SITS AFTER `.sw-hero`, WHICH IS LOAD-BEARING. Both selectors are one class, so specificity
   * ties and SOURCE ORDER decides — written above, this modifier is silently overridden and the
   * monogram gets a 16rem column with no error and nothing to see except too much white space. The
   * same trap `.sw-dialog--reading` records for itself. */
  .sw-hero--identity {
    grid-template-columns: auto minmax(0, 1fr);
  }

  /* THE FICHA’S ROWS BECOME LABEL-BESIDE-VALUE at the same threshold the detail list does, so the
   * two read as one system rather than as two lists that happen to sit in the same dialog. */
  .sw-fact-list__row {
    grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
    gap: var(--space-4);
    align-items: baseline;
  }
}

@container sw-detail (min-width: 34rem) {
  .sw-detail-list__row {
    display: grid;
    grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
    gap: var(--space-4);
    align-items: baseline;
  }
}

/* ---- The source line — content that should read as least-read ----
 *
 * TERRITORIES AND THE SYNCHRONISATION TIMESTAMP WERE A FOURTH `.sw-panel`, which gave them the same
 * visual weight as the customer's contact details and the same heading size as their commercial
 * terms. Nobody opens a customer to find out when the snapshot ran.
 *
 * IT IS STILL A <dl> AND STILL LABELLED — demoting something visually must not demote it
 * structurally. A screen reader gets the same two labelled values; what changed is the type scale
 * and the chrome. The labels are also what make "—" mean "no territories" rather than an
 * unexplained dash.
 *
 * A RULE ABOVE RATHER THAN A PANEL AROUND: one hairline separating it from the content above says
 * "this belongs to the record" without giving it a box of its own. */
.sw-source-line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);

  margin-block: var(--space-5) 0;
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--color-border-subtle);

  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}

.sw-source-line__item {
  display: flex;
  gap: var(--space-2);
}

.sw-source-line__item dt::after {
  /* THE COLON IS PRESENTATION AND BELONGS IN THE STYLESHEET, not in a *Copy.cs. A label is the same
   * word whether it sits above its value or beside it, and putting the punctuation in the copy would
   * make every other rendering of that label carry a colon it does not want. */
  content: ":";
}

.sw-source-line__item dd {
  margin: 0;
  color: var(--color-text-primary);
}

/* `.sw-source-line__item time` WAS HERE AND IS DELETED WITH ITS ONLY ELEMENT — 2026-09-07. It kept
 * the synchronised moment on one line: the es-CO short format is four space-separated parts, so a
 * narrow wrapping row broke a single moment four ways and left the meridiem alone as "m.". That
 * moment left the customer detail at the client's request and this row now holds only Zonas, which
 * has no such hazard.
 *
 * THE HAZARD ITSELF HAS NOT GONE, AND NOTHING GUARDS IT NOW. A moment still renders in
 * Orders/Detail and in the orders listing's table cell, and the browser test that measured this one
 * against its own line-height went with it. Both are recorded in the commit rather than left for
 * somebody to meet. */

/* ---- The shortcut list — Inicio's quick actions ----
 *
 * A LIST OF LINKS, STYLED LIKE THE NAVIGATION ITEMS THEY DUPLICATE. They are the same destinations
 * the sidebar offers, so a reader meeting one here and one there should recognise them as the same
 * thing: icon, label, --radius-md, and a hover that lifts the row rather than underlining the text.
 *
 * --layout-touch-target-min at every width, unlike the table's row action which relaxes at lg. This
 * is a list of primary entry points on the landing page; §9 rule 6's floor applies to all of them on
 * a phone, and there is no width at which a 32px row here would be an improvement. */
.sw-shortcut-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);

  margin: 0;
  padding: 0;
  list-style: none;
}

.sw-shortcut {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-block-size: var(--layout-touch-target-min);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);

  /* --color-text-primary, not --color-text-link. These are navigation, not prose links: a panel of
   * eight blue underlined phrases reads as a paragraph that has come apart. The icon, the row target
   * and the hover surface are what say "this is clickable", which is the same set .sw-sidebar__item
   * uses. */
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  text-decoration: none;

  transition: background-color var(--duration-fast) var(--easing-standard);
}

.sw-shortcut:hover {
  background: var(--color-surface-hover);
}

/* The glyph is quieter than the label it precedes - it is decoration, and aria-hidden in the markup
 * says so. SC 1.4.11 asks 3:1 of it as a graphic, which --color-text-secondary clears on the panel;
 * it is not carrying any meaning the label does not. */
.sw-shortcut .sw-icon {
  color: var(--color-text-secondary);
}

/* `.sw-panel-grid`'s two-column rule USED TO BE HERE, keyed on the viewport. It moved to a
 * container query beside the rule it modifies - see `.sw-detail` above. Nothing else declared a
 * two-column panel grid, so this is a move rather than a removal. */

/* ---- md and up: the header becomes a row and the panels gain a step of padding ----
 *
 * MOBILE-FIRST, so this only ADDS - §6's breakpoint rule. Below md the header is a column with the
 * action under the lede, which is the right shape for a thumb and the wrong one for a 1440px page.
 *
 * --space-6 to --space-8 rather than a new value: §6's scale is the whole vocabulary, and a panel
 * that is comfortable on a phone at 24px looks tight at 32px of available gutter. */
@media (min-width: 768px) {
  .sw-page-header {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-6);
  }

  .sw-panel {
    padding: var(--space-8);
  }

  /* Below this the submit fills the column, which is the right shape for a thumb; here it shrinks
   * to its label, because a 512px-wide button beside a 512px-wide input reads as a second field.
   * Same rule .sw-filters already applies to its own submit, at the same breakpoint. */
  .sw-form-column > .sw-button {
    align-self: flex-start;
  }
}

/* The form around a control that asks before it posts — §8.1, _ConfirmAction.
 *
 * A wrapper with no appearance of its own. It exists because the control has to live in a real
 * <form> carrying its own action, hidden fields and antiforgery token, and a bare <form> would
 * otherwise inherit whatever the surrounding layout does to a block child. A column so the button
 * fills the row on a phone and sizes to its label in the role list's row at md. */
.sw-confirm-action {
  display: flex;
  flex-direction: column;
}

/* ===================================================================================
 * Dialog (modal) — §8.12, and §9's dialog contract
 *
 * A NATIVE <dialog> OPENED WITH showModal(). Focus into the dialog, the focus trap, Esc, and the
 * inert background are the platform's; re-implementing any of them is how a hand-rolled modal ends
 * up with a Tab cycle a screen reader walks straight out of. Only focus RESTORE is scripted, and
 * only because it has been the least consistent part across engines.
 *
 * MOBILE-FIRST, so the FULL-SCREEN SHEET is the base and the centred modal is what `md` adds. §6's
 * rule is that base styles target a phone and media queries only add - a dialog whose base is the
 * desktop card and whose sheet arrives in a max-width query is the same component built backwards.
 *
 * THE UA STYLESHEET HAS TO BE UNDONE EXPLICITLY, and this is the part that is invisible in review.
 * A modal <dialog> is given `margin: auto` (which centres it), plus `max-width` and `max-height` of
 * roughly `calc(100% - 6px - 2em)` - so an untouched dialog can never fill the viewport however
 * wide its content is set. Every one of those is overridden below rather than assumed away.
 * =================================================================================== */

.sw-dialog {
  /* ---- The sheet: full screen, below md ---- */
  inline-size: 100%;
  max-inline-size: 100%;

  /* dvh, not vh. MDN records vh as the LARGE viewport, so on a phone 100vh is taller than what is
   * visible whenever the address bar is showing. The pair and the order are the same as .sw-auth's
   * and for the same reason; the vh line is the fallback for anything older than dvh. */
  block-size: 100vh;
  block-size: 100dvh;
  max-block-size: 100vh;
  max-block-size: 100dvh;

  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;

  background: var(--color-surface-primary);
  color: var(--color-text-primary);

  overflow: hidden;
}

/* ============================ THE `display` IS ON `[open]`, AND IT IS NOT A REFINEMENT ============
 *
 * THIS RULE SHIPPED AS `.sw-dialog { display: flex }` AND IT PUT A DIALOG ON THE PAGE ON ARRIVAL.
 *
 * The user agent hides a closed dialog with `dialog:not([open]) { display: none }`. An AUTHOR
 * declaration beats a user-agent one regardless of specificity - that is the cascade's origin order,
 * not a tie-break - so an unconditional `display` here overrode it and every dialog rendered in
 * normal flow: visible, in the document, with no ::backdrop, no focus trap and no Esc, because
 * nothing had called showModal(). It looked exactly like a stylesheet that had failed to load.
 *
 * WHAT MAKES IT WORTH THIS MANY LINES IS THAT NINE MARKUP TESTS WERE GREEN THROUGH IT. The `open`
 * attribute was never emitted, aria-labelledby always resolved, the triggers always named real ids -
 * every assertion was correct and every one was about the markup, which was correct. The state was
 * only ever wrong in the browser. `DialogBehaviourTests` exists because of this rule.
 *
 * So `display` belongs on `[open]` and nowhere else, and any future declaration that could reveal a
 * closed dialog - `visibility`, `opacity` with a `content-visibility` override, a transition that
 * needs a starting display - belongs there too. `ComponentStylesheetTests` fails the build on an
 * unqualified one. */
.sw-dialog[open] {
  /* CONTENT IS TOP-ALIGNED, AND THAT IS THE KEYBOARD DECISION RATHER THAN A TASTE ONE. §11 item 8
   * records it for the signed-out card and §8.6 for the drawer: when the on-screen keyboard opens
   * the viewport shrinks, and vertically centred content jumps upward under the user's hands
   * mid-entry. A form in a sheet is exactly that case. */
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

/* ---- Open and close, animated — §11 item 21, CLOSED ----
 *
 * THE THREE PIECES ARE ALL REQUIRED, AND LEAVING ANY ONE OUT PRODUCES A DIFFERENT WRONG RESULT.
 *
 *   transition-behavior: allow-discrete   `display` and `overlay` are discrete properties. Without
 *                                         this they snap at the start of the transition, so the
 *                                         element disappears instantly and the fade animates
 *                                         nothing anybody can see.
 *   `overlay` in the property list        the top layer is its own discrete property. Without it
 *                                         the dialog leaves the top layer immediately on close and
 *                                         the closing animation plays UNDERNEATH the page.
 *   @starting-style                       the state to animate FROM on entry. Without it there is
 *                                         no previous value for a display:none element, so opening
 *                                         is instant and only closing animates - which reads as a
 *                                         glitch rather than as a missing feature.
 *
 * It was deferred in v1 for exactly this reason: §11 item 21 recorded that shipping a first version
 * without seeing it on a device is how a modal acquires a flicker nobody can reproduce. It is built
 * now because v2's whole subject is depth and motion, and because the browser suite can watch it.
 *
 * The travel is 8px and the scale is 0.98. Both are deliberately small: a modal that flies in is a
 * modal people wait for.
 * https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style */
.sw-dialog {
  opacity: 1;
  transform: none;

  transition:
    opacity var(--duration-slow) var(--easing-emphasized),
    transform var(--duration-slow) var(--easing-emphasized),
    overlay var(--duration-slow) allow-discrete,
    display var(--duration-slow) allow-discrete;
}

.sw-dialog:not([open]) {
  opacity: 0;
  transform: translateY(8px) scale(0.98);
}

@starting-style {
  .sw-dialog[open] {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
  }
}

/* The backdrop fades with it. A scrim that appears instantly under a dialog that fades in is the
 * pair reading as two separate events. */
.sw-dialog::backdrop {
  opacity: 1;
  transition:
    opacity var(--duration-slow) var(--easing-emphasized),
    overlay var(--duration-slow) allow-discrete,
    display var(--duration-slow) allow-discrete;
}

.sw-dialog:not([open])::backdrop {
  opacity: 0;
}

@starting-style {
  .sw-dialog[open]::backdrop {
    opacity: 0;
  }
}

/* THE BACKDROP READS THE PROMOTED ALIAS, and there is one thing worth knowing about it.
 * ::backdrop did not inherit from its originating element until 2024 - Chrome 122, Safari 17.4,
 * Firefox 123 - and before that a var() here resolved to nothing, the declaration was dropped, and
 * the backdrop rendered the UA default. Every engine we target is well past that. Recorded because
 * the failure is silent and looks like a missing rule rather than a resolution problem.
 * https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop */
.sw-dialog::backdrop {
  background: var(--color-overlay);
}

/* A FORM MAY SPAN THE BODY AND THE FOOTER, and when it does it becomes the dialog's flex column so
 * that the body is still the only thing that scrolls. The invite dialog is that shape: the submit
 * control belongs in the action row and has to stay inside the form it submits.
 *
 * min-block-size: 0 is the load-bearing half. A flex item's default minimum is its content size, so
 * without it the form refuses to shrink, the body never becomes scrollable, and a long dialog
 * overflows the sheet instead - which looks like the overflow rule not working. */
.sw-dialog__form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-block-size: 0;
}

.sw-dialog__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  flex: none;

  padding: var(--space-4);
  border-block-end: 1px solid var(--color-border-subtle);
}

/* The title and its optional sentence, so the close control stays aligned to the top of the row
 * rather than to the middle of a two-line heading. */
.sw-dialog__heading {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* §5's section-heading row - xl, 600, UI face - and the element aria-labelledby points at. It is a
 * real heading rather than a styled div: a dialog whose accessible name comes from a <span> is one
 * a screen-reader user cannot reach with heading navigation once they are inside it. */
.sw-dialog__title {
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-tight);
  color: var(--color-text-primary);
}

/* Icon-only, so §7's rule applies: an accessible name it does not show, and the full 44x44 target
 * below lg even though the glyph is 20px. flex: none because it is a flex item and a declared size
 * is only a starting size - the drawer toggle rendered 31x44 exactly that way. */
.sw-dialog__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;

  inline-size: var(--layout-touch-target-min);
  block-size: var(--layout-touch-target-min);
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.sw-dialog__close:hover {
  background: var(--color-surface-hover);
  color: var(--color-text-primary);
}

/* THE BODY IS THE ONLY THING THAT SCROLLS. The header and the footer stay put, so on a sheet with
 * the keyboard raised the confirm control does not disappear off the bottom with the content. */
.sw-dialog__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  flex: 1 1 auto;

  /* See .sw-dialog__form: a flex item's default minimum is its content size, so this is what makes
   * overflow-y below do anything at all. */
  min-block-size: 0;

  padding: var(--space-4);
  overflow-y: auto;

  /* Momentum scrolling inside the sheet without the page behind it moving once the body reaches
   * its end. The scroll lock on <html> is the other half and carries §8.6's iOS caveat. */
  overscroll-behavior: contain;
}

.sw-dialog__intro {
  color: var(--color-text-secondary);
}

/* Actions last in the DOM and last in the reading order. Column on a phone so each control is a
 * full-width target for a thumb.
 *
 * CANCEL COMES FIRST IN THE MARKUP AND NOTHING REORDERS IT. The first draft put confirm first and
 * used `order: 1` to flip it at md - which desynchronises focus order from visual order, the exact
 * property §8.5 refuses `order` for. It buys nothing here: cancel-then-confirm already lands the
 * primary action at the trailing edge of a flex-end row on desktop AND at the bottom of the column
 * on a phone, which is where both conventions want it. On a destructive confirmation it also means
 * the SAFE control is the one reached first. */
.sw-dialog__footer {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: none;

  padding: var(--space-4);
  border-block-start: 1px solid var(--color-border-subtle);
}

/* A FOOTER ACTION THAT ARRIVES BY AN OUT-OF-BAND SWAP, AND THE SLOT IT ARRIVES INTO — 2026-09-06.
 *
 * The statement dialog's document action names a customer, and this footer is rendered once with the
 * page before any customer has been chosen. So the markup carries an empty element and the fragment
 * fills it. See ReceivableListViewModel.StatementActionSlotId.
 *
 * :empty IS THE WHOLE RULE, AND IT IS THERE BECAUSE THE FOOTER IS A FLEX CONTAINER WITH A GAP. An
 * empty span is still a flex item: it takes a --space-3 gap beside the close button at every width,
 * and below md - where the footer is a column and items stretch - it takes a full-width row of
 * nothing as well. Both are visible, and neither would look like a defect anybody could name.
 *
 * NOT [hidden] AND NOT A SERVER-SIDE CONDITION. The element has to exist at parse time or the swap
 * has no target, and whether it is filled is decided after the page is rendered - so the only thing
 * that can know is the browser, looking at whether it has children. CLAUDE.md section 7 records what
 * an empty data-* marker costs; this is the same shape answered by a selector rather than by an
 * attribute nothing can unset. */
.sw-dialog__footer-slot {
  /* THE SLOT IS A TRANSPORT MECHANISM, NOT A LAYOUT BOX, AND display: contents IS WHAT SAYS SO.
   *
   * MEASURED BEFORE THIS RULE EXISTED: at 320px the swapped-in anchor was 159px wide beside a 282px
   * close button. The footer is a flex COLUMN there and stretches its items, so the SLOT filled the
   * row - and the anchor inside it, being an inline-flex button, sized to its own text. A wrapper
   * that stretches and a child that does not is a stacked pair of two different widths, which is
   * exactly the phone reading this placement was asked for.
   *
   * WHY NOT display: flex ON THE SLOT. It was the first answer and it does not work: the slot would
   * stretch, but its own main axis is horizontal, so the anchor stretches VERTICALLY and keeps its
   * content width. Making it work needs a second declaration on the child, which is a second set of
   * rules for a box that is only here to be a swap target.
   *
   * contents REMOVES THE BOX AND LEAVES THE ELEMENT. The anchor becomes a direct flex item of the
   * footer and gets exactly the treatment the close button gets - stretched in the column below md,
   * pinned to the trailing edge in the row above it - with no rule of its own. The id stays in the
   * DOM, which is all the out-of-band swap needs. */
  display: contents;
}

.sw-dialog__footer-slot:empty {
  display: none;
}

@media (min-width: 768px) {
  .sw-dialog {
    /* ---- The centred modal, md and up ---- */
    inline-size: 100%;
    max-inline-size: var(--layout-form-max-width);

    /* fit-content, NOT auto, AND THE DIFFERENCE IS THE WHOLE HEIGHT OF THE DIALOG. This said
     * `auto` and every centred modal rendered at its maximum: measured 836px against 406px of
     * content — a header and a form — with 430px of empty surface below it. `auto` looks like the
     * neutral value and is not one here. The user-agent stylesheet gives a dialog
     * `position: absolute; inset: 0; margin: auto; height: fit-content`, and for an absolutely
     * positioned box with both block insets set, a block-size of `auto` is resolved by FILLING the
     * containing block, not by measuring the content. So `auto` did not restore the default — it
     * replaced the one declaration that made a dialog shrink to what is in it, and
     * `max-block-size` below then read as the height rather than as a ceiling. */
    block-size: fit-content;
    /* Never taller than the viewport less a comfortable margin, so a long dialog scrolls its body
     * rather than running off the screen. Arithmetic on §6's own scale, not a new value. */
    max-block-size: calc(100dvh - var(--space-16));

    margin: auto;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }

  /* A READ-ONLY DIALOG MAY BE WIDER THAN A WRITE FORM, and it is a modifier rather than a new
   * component: everything else — the sheet below `md`, the backdrop, the trap, the close controls —
   * is unchanged, and only the one measure differs. See --layout-reading-max-width for why the two
   * widths are separate values.
   *
   * IT SITS AFTER `.sw-dialog`, AND THAT IS LOAD-BEARING RATHER THAN TIDINESS. Both selectors are
   * one class, so they have equal specificity and SOURCE ORDER decides. Written above the base rule
   * this modifier is silently overridden by it and the dialog stays 32rem - a defect with no error,
   * no warning and nothing to see except a dialog that did not get wider.
   *
   * IT IS APPLIED TO BOTH QUICK VIEWS SINCE 2026-08-19. This comment said "the product quick view
   * and not the customer one - the customer detail is the same shape and is a candidate, but it is a
   * shipped surface and widening it is its own change with its own look". That change is the one
   * that added the customer hero, and it is what this paragraph predicted. Corrected rather than
   * left, because a comment naming which callers a rule has is a comment that goes stale silently. */
  .sw-dialog--reading {
    max-inline-size: var(--layout-reading-max-width);
  }

  /* A THIRD WIDTH, FOR A FORM WHOSE BODY IS A SELECTION GRID RATHER THAN A COLUMN OF FIELDS.
   *
   * It is a WRITE dialog - a real POST, an antiforgery token, no light dismiss - so `reading` is
   * wrong for it, and `--layout-form-max-width` is the measure of a column somebody types into,
   * which nobody does here. What decides this one's width is how many equal short options fit on a
   * row: at 32rem the option grid can only ever make two columns, whatever its auto-fit says.
   *
   * See --layout-selection-max-width for the arithmetic that produced 44rem, and design-system.md
   * §8.12 for why this is a third case rather than a wider first one.
   *
   * AFTER BOTH RULES ABOVE, WHICH IS LOAD-BEARING. Three single-class selectors on one element:
   * specificity ties and SOURCE ORDER decides, so a modifier written above the base is silently
   * overridden with no error and nothing to see except a dialog that did not get wider. That has
   * now been recorded three times in this file and hit once. */
  .sw-dialog--selection {
    max-inline-size: var(--layout-selection-max-width);
  }

  .sw-dialog__header,
  .sw-dialog__body,
  .sw-dialog__footer {
    padding: var(--space-6);
  }

  .sw-dialog__footer {
    flex-direction: row;
    justify-content: flex-end;

    /* THE ACTION ROW ALIGNS ON ITS CONTROLS' BASELINES ONCE IT CARRIES MORE THAN BUTTONS. A labelled
     * select is taller than a button, and `stretch` — flex's default — would draw a button as tall
     * as the select beside it. */
    align-items: flex-end;
  }

  /* AN OPTION THAT QUALIFIES THE ACTION, KEPT AT THE START OF THE ROW.
   *
   * `margin-inline-end: auto` rather than a second justify-content: the buttons stay pinned to the
   * trailing edge where §8.12 puts them, and this takes the slack. One declaration, and it does not
   * move the primary action — which is the thing every reader has already learned the position of.
   *
   * Below `md` it does nothing: the footer is a column there and the select simply stacks above the
   * buttons, which is the order a reader meets them in anyway. */
  .sw-dialog__footer-lead {
    margin-inline-end: auto;
  }
}

/* ===================================================================================
 * The signed-out frame — design-system.md §11 item 8
 * =================================================================================== */

.sw-auth {
  /* TOP-ALIGNED, NOT VERTICALLY CENTRED, and this is the one layout decision with a reason that
   * is not aesthetic: when the mobile keyboard opens the viewport shrinks, and a vertically
   * centred form jumps upward under the user's hands mid-entry. */
  --sw-auth-card-width: 400px;

  display: flex;
  flex-direction: column;
  align-items: center;

  /* THE PAIR IS THE POINT, AND THE ORDER IS THE POINT. MDN records vh as equivalent to lvh - the
   * LARGE viewport - so on a phone 100vh is taller than what is visible whenever the address bar
   * is showing, and the page overflows for that reason alone. dvh follows the visible area and
   * has been Baseline since 2023; the vh line above it is the fallback for anything older.
   *
   * min-height, never height, and no overflow: hidden anywhere. WCAG 1.4.10 requires content to
   * reflow and SCROLL at 320px and at 200% zoom. The budget below is a target, not a lock. */
  min-height: 100vh;
  min-height: 100dvh;

  /* A HEIGHT BUDGET: the lockup plus the card should clear a 1366x768 laptop without scrolling.
   * This was --space-10 above and --space-16 below - 104px of nothing on a page holding two
   * fields. */
  padding: var(--space-8) var(--space-4);

  /* Layer 3, because #F6F8F7 on #FFFFFF is 1.03:1 and that is not a separation. */
  background: var(--color-auth-page-background);
}

/* At md and above there is room to centre without the keyboard case applying - a physical
 * keyboard does not resize the viewport. */
@media (min-width: 768px) {
  .sw-auth {
    justify-content: center;
    padding-block: var(--space-8);
  }
}

/* And tighter still on a phone, where the keyboard takes most of the viewport. */
@media (max-width: 479px) {
  .sw-auth {
    padding-block: var(--space-6);
  }
}

/* display:flex, not the default block, and it is a height fix rather than a layout preference.
 * The lockup is an inline-level box (.sw-brand is inline-flex), so in a block container it sits
 * on a text baseline and the line box adds the strut's descent underneath it - about 9px of gap
 * that nothing occupies. A flex container has no line box and no baseline to sit on.
 *
 * The font-size that used to be here went with it: there is no text in this element. */
.sw-auth__brand {
  display: flex;
  margin-block-end: var(--space-6);
}

.sw-auth__card {
  width: 100%;
  max-width: var(--sw-auth-card-width);

  /* GENEROUS. Cramped is the single clearest tell of a template, and this is the first thing a
   * pharmacy owner sees. */
  padding: var(--space-8);

  border-radius: var(--radius-lg);
  background: var(--color-surface-primary);
  box-shadow: var(--shadow-lg);
}

/* Below sm the card is nearly the full width of the phone, so 32px of padding on each side eats
 * a fifth of it. --space-6 keeps the fields usable without reading as cramped. */
@media (max-width: 479px) {
  .sw-auth__card {
    padding: var(--space-6);
  }
}

/* --space-6 to the first field - larger than the --space-5 between fields, so the heading reads
 * as a heading rather than as another row. It carried --space-2 while a supporting line followed
 * it; that line is gone (NN/g: remove what can be derived), so the gap moves here.
 *
 * semibold rather than bold: at 24px in the display face, bold is heavier than this page needs.
 * The tracking token is the only new token in this change - see tokens.css and §5. */
.sw-auth__title {
  margin-block-end: var(--space-6);
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-semibold);

  /* §5's application table assigns --line-height-tight to headings. This inherited
   * --line-height-normal from body, so the document was being contradicted rather than applied -
   * 1.5 on a 24px heading is body leading on display type. Worth 6px of the height budget too,
   * but the reason is the table. */
  line-height: var(--line-height-tight);
  letter-spacing: var(--letter-spacing-tight);
  color: var(--color-text-primary);
}

/* The supporting line, for the pages that have something to say the form does not.
 *
 * DELETED WITH THE LOGIN PAGE'S LEDE AND NEEDED AGAIN BY ForgotPassword, which is the whole
 * argument for the class-coverage theory: the markup went back before the rule did, and the only
 * thing that noticed was a test comparing what a page renders against what its stylesheets define.
 *
 * The title carries --space-6 to the first field. Where a lede follows it, the title tightens to
 * --space-2 and the lede owns the --space-6 instead, so heading and lede read as one block with
 * the larger gap after the pair. Written with :has() rather than a negative margin - the negative
 * version worked and said nothing about why. Baseline since December 2023. */
.sw-auth__title:has(+ .sw-auth__lede) {
  margin-block-end: var(--space-2);
}

.sw-auth__lede {
  margin-block-end: var(--space-6);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

.sw-auth__form {
  display: flex;
  flex-direction: column;

  /* TIGHTER THAN THE TITLE GAP, and deliberately: --space-5 between field groups against
   * --space-6 above them. Loose at the top, tighter through the middle. Equal gaps everywhere
   * is what makes a form read as a spreadsheet. */
  gap: var(--space-5);
}

/* ONE call to action. The submit is full-width, lg, primary; the reset link below it is
 * tertiary, smaller and secondary-coloured. Two controls of equal weight is what makes a screen
 * feel indecisive.
 *
 * --space-6 above it - more air than between the fields, so the button reads as the end of the
 * form rather than as another row in it. */
.sw-auth__actions {
  display: flex;
  flex-direction: column;
  align-items: center;

  /* Submit to the reset link. */
  gap: var(--space-5);

  /* ON TOP OF THE FORM'S --space-5 GAP, giving --space-6 from the remember-me row. Written as
   * the difference rather than as the total, because the gap applies to every form child and a
   * margin of --space-6 here would produce 44px, not 24px. */
  margin-block-start: var(--space-1);
}

/* THE CARD FOOTER WAS HERE AND IS GONE. It existed because a card ending in a button feels cut
 * off; the card no longer ends in a button, because the reset link is below it. It had also been
 * built with the brand partial rather than as small text, so the lockup rendered twice on one
 * screen - above the card and inside it. */

/* ===================================================================================
 * Gallery layout — /design-system only
 *
 * NOT A COMPONENT, and it lives here rather than in app-shell.css for the reason §10 rule 10
 * gives: the gallery is served through the signed-in layout today, but a rule that only exists
 * in app-shell.css is invisible the moment anything renders it outside that frame. components.css
 * is loaded by both layouts, so putting it here cannot produce the silent-absence failure the
 * rule was written after.
 *
 * One class, doing one thing: lay specimens out in a row that wraps, so every state of a
 * component is visible at once rather than stacked down a page nobody scrolls.
 * =================================================================================== */

.sw-gallery__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-4);

  margin-block-end: var(--space-6);
}

/* ===================================================================================
 * Data table — §8.5
 *
 * THE ONE DECLARATION THIS COMPONENT TURNS ON IS `display`, AND THE WRONG VALUE IS INVISIBLE.
 *
 * The stacked-card layout below `md` is built with `grid`. The recipe everyone reaches for is
 * `display: block` on the table, its rows and its cells - and that REMOVES THE TABLE FROM THE
 * ACCESSIBILITY TREE. A screen reader stops announcing which header a cell belongs to, which is
 * the only thing that makes a cell of data mean anything. Nothing on screen changes.
 *
 * Measured across engines rather than assumed
 * (https://adrianroselli.com/2018/02/tables-css-display-properties-and-aria.html):
 *
 *   | display        | Chrome 80+ | Firefox | Safari <17 |
 *   |----------------|------------|---------|------------|
 *   | flex / grid    | keeps      | keeps   | dropped    |
 *   | inline-block   | keeps      | keeps   | dropped    |
 *   | contents       | keeps      | DROPS   | dropped    |
 *   | block          | DROPS      | DROPS   | dropped    |
 *
 * `grid` is the only value safe in every engine still shipping, and Safari 17 fixed the rest.
 * `contents` is excluded for Firefox alone.
 *
 * ARIA IS NOT THE REMEDY. Roles are what you add once you have already chosen wrongly, and the
 * `grid` role family is for interactive widgets rather than for data - there is no ARIA spelling
 * of "this cell belongs to that column header" that a real <th scope="col"> does not already
 * carry. There are no roles on this markup and there must never be any.
 *
 * Verified by measurement, not by reading this comment:
 *   dotnet run --project tools/Smartwave.ResponsiveProbe -- --accessibility-tree
 *
 * ---------------------------------------------------------------------------------
 * MOBILE-FIRST, so the CARD layout is the base and the TABLE is what `md` adds. §7's breakpoint
 * rule is that base styles target a phone and media queries only add; a component whose base is
 * the desktop table and whose phone layout arrives in a max-width query is the same component
 * built backwards, and it renders the wrong thing at any width the queries miss.
 * =================================================================================== */

.sw-table {
  --sw-skeleton-duration: 1.4s;

  display: grid;
  gap: var(--space-3);

  width: 100%;
  margin-block-end: var(--space-6);

  font-size: var(--font-size-sm);
}

/* THE COLUMN GEOMETRY EXISTS ONLY WHERE THERE ARE COLUMNS, AND THIS RULE IS NOT A TIDY-UP.
 *
 * `.sw-table` above is `display: grid` for the card layout, so a <colgroup> and its <col> children
 * are in-flow children of a GRID CONTAINER. Their user-agent display is `table-column-group` and
 * `table-column`; a grid item's display is blockified, and an internal-table box that is blockified
 * becomes a block box. A block box in a grid takes a track - so without this the card layout gains
 * an empty row above the first card, of a table's worth of columns, on every phone.
 *
 * MEASURED, NOT REASONED. That last paragraph is how the spec reads and it is not why this rule is
 * here: `TableColumnGeometryTests` drives a real Chromium at 767px and asserts the colgroup and
 * every col compute to `display: none` and occupy a zero-height box, and that the first card sits
 * directly below the sort strip. Red-checked by deleting this rule and watching the gap appear.
 * The last two rounds on this component were lost to reasoning about CSS instead of running it. */
.sw-table__colgroup,
.sw-table__col {
  display: none;
}

.sw-table__caption {
  /* A caption's text-align is `center` in some UA sheets. */
  text-align: start;

  color: var(--color-text-secondary);
  font-weight: var(--font-weight-semibold);
}

.sw-table__head {
  display: grid;
}

/* THE SORT CONTROLS SURVIVE THE CARD LAYOUT, and that is a judgement rather than a given. §8.5
 * says the table becomes cards and says nothing about the header. Hiding <thead> is the usual
 * move and it silently removes sorting on the one device where the column set is smallest and
 * ordering matters most. Visually hiding it is worse: the buttons keep focus, so a keyboard user
 * tabs into controls nobody can see.
 *
 * So the header row becomes a wrapping strip of sort buttons above the cards. Same elements, same
 * semantics, no new component. */
.sw-table__head-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.sw-table__header {
  color: var(--color-text-secondary);
  font-weight: var(--font-weight-semibold);
  text-align: start;
}

/* A header with no control says nothing useful in the card strip - its label is already on every
 * cell - so it is not DRAWN there. It is still ANNOUNCED, and the difference between those two is
 * the whole reason this component is measured.
 *
 * THIS WAS `display: none` AND THE PROBE CAUGHT IT. At 375px the tree came back with
 * `columnheader×4` against `×5` at 1024: the one non-sortable column, Estado, had been removed
 * from the accessibility tree, so every status badge on a phone was a cell belonging to no column.
 * The layout was correct, the markup was correct, the roles on everything else were correct, and
 * nothing on screen looked wrong.
 *
 * Clipped rather than removed. Safe here precisely because this branch is the headers with NO
 * control in them - visually hiding a focusable element is the separate defect noted above, and a
 * header without a sort button has nothing to focus. */
.sw-table__header:not(:has(.sw-table__sort)) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.sw-table__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);

  /* §9 rule 6: 44x44 minimum below `lg`. The strip is the only interactive part of the table on a
   * phone, so this is the whole of that rule's surface here. */
  min-height: 44px;
  padding-inline: var(--space-3);

  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-secondary);

  color: var(--color-text-secondary);
  font: inherit;
  font-weight: var(--font-weight-semibold);

  /* THE CONTROL BECAME AN <a> ON 2026-09-08 AND AN ANCHOR CARRIES AN UNDERLINE A BUTTON DOES NOT.
   * Everything above was written for a <button>, where `font: inherit` and the absence of a
   * decoration were enough. The sort control is a link now - the sort is state and belongs in the
   * address - so the decoration has to be turned off explicitly or the header strip grows an
   * underline nothing asked for. `cursor: pointer` below is now redundant on an anchor and is kept
   * because this class has to look identical whichever element carries it. */
  text-decoration: none;

  cursor: pointer;
}

.sw-table__sort:hover {
  background: var(--color-surface-hover);
}

.sw-table__sort-icon {
  flex: none;
  inline-size: 1rem;
  block-size: 1rem;
}

.sw-table__header--sorted .sw-table__sort {
  color: var(--color-text-primary);
}

.sw-table__body {
  display: grid;
  gap: var(--space-3);
}

/* ---- The card ---- */

.sw-table__row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-2) var(--space-3);

  padding: var(--space-4);

  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-primary);
}

.sw-table__row--selected {
  background: var(--color-surface-selected);
}

.sw-table__cell {
  /* Every cell but the two below spans the card and lays its label beside its value. */
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: 40% 1fr;
  gap: var(--space-3);
  align-items: baseline;
}

/* THE TWO CELLS §8.5 NAMES, PLACED EXPLICITLY RATHER THAN REORDERED. `order` moves them visually
 * and leaves them where they were for a screen reader, which is the exact divergence this
 * component exists to avoid. Explicit grid placement moves the box; the DOM order a reader hears
 * is untouched either way. */
.sw-table__cell--identifier {
  display: block;
  grid-column: 1;
  grid-row: 1;

  color: var(--color-text-primary);
  font-weight: var(--font-weight-semibold);
}

.sw-table__cell--status {
  display: block;
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
}

/* ---- The row action — §8.5, and ADR-0026 for why it is shaped like this ----
 *
 * §8.5 SETTLED TWO THINGS AND LEFT THIS ONE OPEN: "A card is not tappable as a whole and carries no
 * action of its own... When a row action IS specified, it arrives as an explicit control inside the
 * card, subject to §9 rule 6." So the row is never the control - making the whole row or the whole
 * card a link invents navigation §8.5 explicitly refuses, and it takes the row's text with it into
 * one enormous link name.
 *
 * IT IS AN OUTLINED CONTROL ON A CARD AND A PLAIN LINK IN A TABLE, which is the same shape shift
 * .sw-table__sort already makes and for the same reason: on a phone the card has no column heading
 * to explain it and the control has to look like one, while twenty outlined buttons down a desktop
 * table is a column of boxes shouting over the data they belong to. */
.sw-table__cell--action {
  display: block;
  grid-column: 1 / -1;
}

/* The label above a control repeats what the control's own text says. Same reasoning as the
 * identifier and the badge above, which is why it is the same declaration. */
.sw-table__cell--action .sw-table__label {
  display: none;
}

.sw-table__action {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);

  /* §9 rule 6. The card layout runs below md and the touch minimum runs below lg, so this holds
   * through both and is released at lg where the pointer is a mouse. */
  min-block-size: var(--layout-touch-target-min);
  padding-inline: var(--space-3);

  border: 1px solid var(--color-action-secondary-border);
  border-radius: var(--radius-md);

  /* Measured by the dark sweep at 4.00:1 as --color-action-primary, against the STRIPED row surface
   * rather than the panel - which is the reason the number was worse here than anywhere else and
   * the reason guessing at it from the stylesheet gave the wrong answer. --color-tone-brand-fg
   * measures 5.56:1 on that same surface. See .sw-button--secondary. */
  color: var(--color-tone-brand-fg);
  font-weight: var(--font-weight-medium);
  text-decoration: none;
}

/* THE SAME TONAL FILL FOR THE POINTER AND FOR THE KEYBOARD. :focus-visible already draws §9's ring
 * from base.css, and the ring says "this is where you are"; the fill says "this is a control you can
 * press", which is the thing a hover was communicating to everybody except the keyboard. Both, so
 * the two ways of arriving at the action look like the same control.
 *
 * NO BACKGROUND AT REST, deliberately. §8.1 keeps emphasis for one primary control per view, and an
 * action repeated on 950 rows is not it - a filled button per row would out-shout whatever is. */
.sw-table__action:hover,
.sw-table__action:focus-visible {
  background: var(--color-action-secondary-hover);
}

.sw-table__label {
  color: var(--color-text-secondary);
}

/* ABSENCE, DRAWN QUIETER THAN THE DATA. Two Clientes columns in five are empty at the source, so at
 * full text weight a column of dashes competes with the figures beside it for the same attention.
 * The token's own annotation is secondary text; this is exactly that.
 *
 * The dash is still rendered and still announced - a blank cell reads as a rendering fault, and an
 * aria-hidden one leaves a screen-reader user unable to tell "no value" from "the row ended". */
.sw-table__placeholder {
  color: var(--color-text-secondary);
}

/* The identifier and the badge lead the card; a label above them repeats what the card plainly
 * is. Hidden rather than not rendered, because at `md` the same span is hidden for every column
 * and one rule is easier to keep true than two code paths. */
.sw-table__cell--identifier .sw-table__label,
.sw-table__cell--status .sw-table__label {
  display: none;
}

.sw-table__cell--numeric .sw-table__value {
  /* TABULAR FIGURES, INVISIBLE UNTIL THE NUMBERS DIFFER IN WIDTH. In a proportional face a `1` is
   * narrower than an `8`, so a column of currency drifts and the eye can no longer compare
   * magnitudes down it. On the value rather than the cell, so the label stays proportional. */
  font-variant-numeric: tabular-nums;
}

/* ---- A tone on a value — §8.4's status colours, carried by a figure rather than by a pill ----
 *
 * WHY A VALUE AND NOT A BADGE, WHICH IS THE DECISION RATHER THAN THE RULE. The catalog's
 * `Disponible` column rendered a SENTENCE for two of its three states, first as text and then as a
 * badge. Neither fits: measured on /commercial/catalog, `Sin disponibilidad` is 140px and
 * `Sin disponibilidad (faltan 15)` is 213px, in a cell that is 109px at 1024 and never more than
 * 181 - and `table-layout: fixed` means the column cannot grow, so both were painted over the
 * status badge beside them. A column headed with a noun and aligned numeric holds a number; the
 * state it is in is reinforcement, and reinforcement is what a tone is for. Open question 125.
 *
 * REINFORCEMENT ONLY, SAME AS .sw-kpi__delta--* ABOVE, AND THE SAME WCAG CLAUSE. SC 1.4.1 forbids
 * colour as the sole carrier of meaning; the delta pairs its tone with an arrow and a word, and this
 * pairs it with the figure itself - `0 UND` and `-15 UND` are distinguishable from `228 UND`, and
 * from each other, with no colour at all.
 *
 * ALL SIX DECLARED THOUGH ONE IS USED, which is this stylesheet's own argument for the twelfths
 * scale: `StatusTone` is the mechanism, a caller needing --error should not also need a stylesheet
 * edit, and a partial set is how the next author concludes the names are arbitrary.
 *
 * STATUS TOKENS RATHER THAN TONE TOKENS, and DesignCalibrationTests enforces the difference: a tone
 * is variety and a status is a claim about a record. "Nothing free to sell" is a claim about a
 * record. */
.sw-table__value--success  { color: var(--color-status-success-fg); }
.sw-table__value--error    { color: var(--color-status-error-fg); }
.sw-table__value--warning  { color: var(--color-status-warning-fg); }
.sw-table__value--info     { color: var(--color-status-info-fg); }
.sw-table__value--pending  { color: var(--color-status-pending-fg); }
.sw-table__value--inactive { color: var(--color-status-inactive-fg); }

/* ---------------------------------------------------------------------------
   Product hero — the detail's opening region. design-system §8.5-adjacent.

   NO NEW TOKENS. Every value below is an existing colour, radius, space or type token; the only
   literals are aspect-ratio and percentage geometry, which are not colours and have nothing to
   declare in tokens.css.
   --------------------------------------------------------------------------- */

.sw-hero {
  display: grid;
  gap: var(--space-5);
  margin-block-end: var(--space-6);
}

.sw-hero__media {
  /* THE SQUARE IS THE POINT. Catalogue photographs arrive at assorted dimensions and a hero that
   * changes height per product makes the identity beside it jump as a reader pages through. A fixed
   * ratio with `contain` gives every product the same frame. */
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;

  background: var(--color-surface-image);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}

.sw-hero__image {
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
}

.sw-hero__placeholder {
  color: var(--color-text-disabled);
}

.sw-hero__expand {
  /* BOTTOM-RIGHT, INSIDE THE FRAME. Absolute against `.sw-hero__media`, which is why that element
   * is positioned — the only reason. */
  position: absolute;
  inset-block-end: var(--space-2);
  inset-inline-end: var(--space-2);

  /* THE TOUCH TARGET IS THE TOKEN, not the icon's size. §6's minimum applies to a control whose
   * visible art is 20px, and shrinking it to fit the glyph is how an icon button becomes unusable
   * on a phone. */
  min-inline-size: var(--layout-touch-target-min);
  min-block-size: var(--layout-touch-target-min);

  display: inline-flex;
  align-items: center;
  justify-content: center;

  color: var(--color-text-secondary);
  background: var(--color-surface-primary);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}

.sw-hero__expand:hover {
  color: var(--color-text-primary);
  background: var(--color-surface-hover);
}

/* ---- The monogram hero — a customer, which has no photograph ----
 *
 * A CUSTOMER HAS NO IMAGE AND NEVER WILL. Nothing in the source carries one and nothing in Smartwave
 * collects one, so the product hero's `.sw-hero__placeholder` — a picture frame with a picture glyph
 * in it — would be this surface announcing a missing photograph that was never missing.
 *
 * INITIALS SAY THE OPPOSITE: this is what we have, and it is complete. The circle is deliberately
 * NOT the square `.sw-hero__media` frame, so the two heroes cannot be mistaken for the same object
 * with one image failing to load.
 */
.sw-hero__monogram {
  /* 4rem, not a token, and the same class of literal `.sw-hero__media`'s aspect-ratio is: this is
   * geometry rather than spacing, and it has nothing to declare in tokens.css. */
  inline-size: 4rem;
  block-size: 4rem;
  flex: none;

  display: flex;
  align-items: center;
  justify-content: center;

  border-radius: var(--radius-full);
  background: var(--color-status-success-bg);
  border: 1px solid var(--color-status-success-fg);
  color: var(--color-status-success-fg);

  font-family: var(--font-family-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  line-height: 1;

  /* THE BORDER IS WHY THIS IS LEGIBLE AT ALL, and it is the same correction the pills carry: a
   * `-50` tint measures about 1.06:1 against a white panel (§11 item 20, open question 64), so the
   * fill alone would be an invisible circle behind coloured letters. */
}

.sw-hero__name {
  /* THE ONE PLACE THE DISPLAY FACE IS EARNED. §5 permits Manrope at --font-size-xl and up; this is
   * the largest thing in the dialog and the first thing a reader looks for. */
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);
  letter-spacing: var(--letter-spacing-tight);

  margin-block: var(--space-2) var(--space-3);
}

.sw-hero__pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-end: var(--space-5);
}

/* ---- Pills ---------------------------------------------------------------
   A PILL IS NOT A BADGE AND THE DIFFERENCE IS DELIBERATE. §8.4's badge states a row's status in a
   table; these label an object in a hero, several at a time, and one of them is a product code
   rather than a state. Sharing the class would make every future badge change a hero change.

   EVERY PILL CARRIES A BORDER IN ITS OWN FOREGROUND COLOUR, and that is a correction rather than a
   decoration. design-system §11 item 20 and open question 64 record that a `-50` tint measures
   about 1.06:1 against a white panel: the fill is invisible and what a reader sees is coloured
   text. A tinted pill drawn on that fill alone would reproduce a defect this project has already
   found once, on the user-administration surface. The border is what makes it a shape.
   --------------------------------------------------------------------------- */

.sw-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);

  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid currentcolor;

  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-tight);
}

.sw-pill--code {
  /* THE CODE IS NOT A STATUS, and it takes the success family only because it is the brand's own
   * green. Nothing about a product code is "successful"; the tokens are named for the palette they
   * came from and this is the one place that distinction is worth stating. */
  color: var(--color-status-success-fg);
  background: var(--color-status-success-bg);

  /* NO MONOSPACE FACE. This carried `var(--font-family-mono)` and there is no such token — the
   * design system ships two families, UI and display, and a third would be a type decision needing
   * §5 and a sign-off rather than a line in a component rule. `tabular-nums` gets what a code
   * actually needs from the face already loaded. */
  font-variant-numeric: tabular-nums;
  align-self: start;
}

.sw-pill--neutral {
  color: var(--color-text-secondary);
  background: var(--color-surface-secondary);
}

/* A TONE RATHER THAN A STATUS, which is the whole reason it is not `--info`. A price list is a
 * classification the customer carries; it is neither good nor bad nor a thing that went wrong, and
 * the status family would colour it as one. `--color-tone-teal-*` is the pair §8's KPI chip already
 * uses for exactly that job.
 *
 * MEASURED IN BOTH THEMES, §9: ≈4.51:1 on light and ≈7.88:1 on dark. The light figure is the
 * tightest pair in the system - it clears AA by 0.01 - so this modifier is for a chip's own text
 * and nothing smaller. */
.sw-pill--teal {
  color: var(--color-tone-teal-fg);
  background: var(--color-tone-teal-bg);
}

.sw-pill--success {
  color: var(--color-status-success-fg);
  background: var(--color-status-success-bg);
}

.sw-pill--warning {
  color: var(--color-status-warning-fg);
  background: var(--color-status-warning-bg);
}

/* A PERMITTED BACKORDER IS NOT A WARNING — ADR-0069. The picker draws this where a product has
 * nothing left and the ERP allows selling it anyway: the line goes on backorder, which is an
 * ordinary commercial state in this trade. Colouring it amber would teach the reader to dismiss the
 * amber that means the product cannot be sold at all.
 *
 * THE SAME PAIR THE INFO ALERT USES, and not a new one. */
.sw-pill--info {
  color: var(--color-status-info-fg);
  background: var(--color-status-info-bg);
}

.sw-pill--inactive {
  color: var(--color-status-inactive-fg);
  background: var(--color-status-inactive-bg);
}

.sw-pill--status .sw-pill__dot {
  /* SOLID, NOT currentcolor. The dot is the one part that has to read at a glance, and the -fg on a
   * -bg is a text contrast rather than a shape one. */
  inline-size: 0.5rem;
  block-size: 0.5rem;
  border-radius: var(--radius-full);
  background: currentcolor;
}

/* ---- Stat tiles ---------------------------------------------------------- */

.sw-stat-row {
  /* auto-fit WITH A MINIMUM, so three tiles sit in a row on a wide container and wrap to two and
   * then one as it narrows - without a breakpoint, and without a count in the stylesheet that has
   * to agree with how many tiles authorization happens to have rendered. */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--space-3);
}

.sw-stat {
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-secondary);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

/* A ROW THAT DOES NOT WRAP, for a set small enough to stay on one line at every width.
 *
 * THE DEFAULT auto-fit ROW WRAPS 2 + 1 ON A PHONE and leaves a half-width orphan, which reads as
 * unfinished rather than as a layout. Three tiles carrying small integers do not need 8rem each:
 * at 320 they get about 90px, which holds a four-digit count and its caption.
 *
 * A MODIFIER RATHER THAN A CHANGE TO .sw-stat-row, because the other two callers are hero rows
 * whose tile COUNT varies with permissions - the product detail draws up to five - and forcing
 * those onto one line is what auto-fit exists to avoid.
 */
.sw-stat-row--fit {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* THE CAPTION IS WHAT OVERFLOWS FIRST, not the figure. It is uppercase with wide tracking, so it
 * wraps to two lines in a narrow tile rather than clipping - and the tiles keep equal heights
 * because the row is a grid. */
.sw-stat-row--fit .sw-stat {
  padding: var(--space-3);
}

.sw-stat-row--fit .sw-stat__figure {
  font-size: var(--font-size-lg);
}

.sw-stat--accent {
  color: var(--color-status-success-fg);
  background: var(--color-status-success-bg);
  border-color: currentcolor;
}

.sw-stat__label {
  /* THE UPPERCASE LABEL IS EXACTLY WHAT --letter-spacing-wide EXISTS FOR - its own note names a
   * KPI's caption as the case. */
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
  color: var(--color-text-secondary);
  margin-block-end: var(--space-1);
}

.sw-stat--accent .sw-stat__label {
  color: inherit;
}

.sw-stat__figure {
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);
  font-variant-numeric: tabular-nums;
}

/* A TILE WHOSE VALUE IS NOT A NUMBER. A salesperson code — "V-BROWSER" — set at --font-size-xl is
 * a piece of text pretending to be a figure, and it is the value most likely to overflow its tile.
 * One step down, and it wraps rather than pushing the row. `tabular-nums` is left in place: it is
 * inert on letters and the value is a dash for a customer with no rep, which lines up with the two
 * real figures beside it. */
.sw-stat__figure--text {
  font-size: var(--font-size-base);
  overflow-wrap: break-word;
}

/* ---------------------------------------------------------------------------
   Lightbox — the expanded product image, a layer over the detail dialog.

   `display` GOES ON `[open]`, AND §8.12 RECORDS WHY IN CAPITALS. An unconditional `display` on a
   `<dialog>` overrides the user agent's `dialog:not([open]) { display: none }` — an author
   declaration beats a user-agent one regardless of specificity — and every lightbox on every page
   would render in normal flow. That shipped once already, on the invite dialog.
   --------------------------------------------------------------------------- */

.sw-lightbox {
  max-inline-size: 90vw;
  max-block-size: 90dvh;
  padding: var(--space-2);
  background: var(--color-surface-image);
  border: none;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.sw-lightbox[open] {
  /* POSITIONED ONLY WHEN OPEN, so the close control has something to sit against and a closed
   * dialog gains no box of its own. */
  position: relative;
}

.sw-lightbox::backdrop {
  background: var(--color-overlay);
}

.sw-lightbox__image {
  display: block;
  max-inline-size: 100%;

  /* The dialog's own cap less its padding, so a tall photograph fits rather than pushing the
   * close control off the bottom of the viewport. */
  max-block-size: calc(90dvh - var(--space-8));
  object-fit: contain;
}

.sw-lightbox__close {
  position: absolute;
  inset-block-start: var(--space-2);
  inset-inline-end: var(--space-2);

  /* The touch minimum again — the same token, and the same reason as the expand control. */
  min-inline-size: var(--layout-touch-target-min);
  min-block-size: var(--layout-touch-target-min);

  display: inline-flex;
  align-items: center;
  justify-content: center;

  color: var(--color-text-primary);
  background: var(--color-surface-primary);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Tabs — the product detail's three regions. No new tokens.
   --------------------------------------------------------------------------- */

.sw-tabs__list {
  display: flex;
  gap: var(--space-1);

  /* WRAPS. THIS SHIPPED AS overflow-x: auto AND THE THIRD TAB WAS UNREACHABLE ON A PHONE.
   *
   * The reasoning recorded here was that wrapping "makes the list two rows deep and pushes the
   * panel below the fold", and confining the scroll to the strip was the lesser evil. That traded a
   * layout cost for a REACHABILITY cost, which is not a trade: a horizontally scrollable strip with
   * no visible affordance - no scrollbar on a touch device, no fade, no arrows - is a control the
   * reader has no way to know is there. Ficha tecnica simply did not exist on a narrow dialog.
   *
   * Two rows is a worse layout. An unreachable tab is a broken one, and §6’s no-horizontal-scroll
   * rule was never the thing at stake, because the scroll was inside the strip either way. */
  flex-wrap: wrap;

  border-block-end: 1px solid var(--color-border-subtle);
  margin-block-end: var(--space-5);
}

.sw-tabs__tab {
  /* THE TOUCH MINIMUM IS THE TOKEN. §6 applies it to every control, and a tab strip is where it is
   * most often skipped because the label looks big enough. */
  min-block-size: var(--layout-touch-target-min);

  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;

  padding: var(--space-2) var(--space-4);
  background: none;
  border: none;

  /* THE INDICATOR IS A TRANSPARENT BORDER THAT GAINS A COLOUR, not a border that appears. A border
   * that only exists when selected changes the element's height, and the whole strip shifts by a
   * pixel every time a reader changes tab. */
  border-block-end: 2px solid transparent;

  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  cursor: pointer;
}

.sw-tabs__tab:hover {
  color: var(--color-text-primary);
}

.sw-tabs__tab[aria-selected="true"] {
  /* KEYED ON THE ARIA STATE, NOT ON A CLASS THE SCRIPT ADDS. One source of truth: a tab that looks
   * selected and does not say so to a screen reader is the exact divergence this avoids, and it
   * cannot happen when the same attribute drives both. */
  color: var(--color-text-primary);
  border-block-end-color: var(--color-action-primary);
}

.sw-tabs__heading {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-secondary);
  margin-block-end: var(--space-4);
}

.sw-tabs__count {
  padding: 0 var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-surface-secondary);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}

/* ---- Proportional bars ---------------------------------------------------- */

.sw-bar-list {
  display: grid;
  gap: var(--space-4);
  list-style: none;
  padding: 0;
  margin: 0;
}

.sw-bar-list__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-1) var(--space-4);
  align-items: baseline;
}

.sw-bar-list__label {
  font-weight: var(--font-weight-medium);
}

.sw-bar-list__figure {
  font-variant-numeric: tabular-nums;
  color: var(--color-text-secondary);
}

.sw-bar {
  grid-column: 1 / -1;
  block-size: 0.5rem;
  border-radius: var(--radius-full);
  background: var(--color-surface-secondary);
  overflow: hidden;
}

.sw-bar__fill {
  /* WIDTH IS SET BY SCRIPT, AND ZERO IS THE HONEST STARTING VALUE. Defaulting to 100% would draw a
   * full bar for every warehouse if the script never ran — a wrong figure that looks like a right
   * one. Zero is visibly wrong, which is what an unapplied width should be. */
  display: block;
  inline-size: 0;
  block-size: 100%;
  background: var(--color-action-primary);
}

/* ---- Price cards ---------------------------------------------------------- */

.sw-price-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--space-3);
}

.sw-price-card {
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-secondary);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

.sw-price-card__label {
  font-size: var(--font-size-xs);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
  color: var(--color-text-secondary);
  margin-block-end: var(--space-1);
}

.sw-price-card__figure {
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* ---- The ficha's labelled rows -------------------------------------------- */

.sw-fact-list {
  display: grid;
  margin: 0;
}

.sw-fact-list__row {
  display: grid;
  gap: var(--space-1);
  padding-block: var(--space-3);
}

.sw-fact-list__row + .sw-fact-list__row {
  /* THE DIVIDER IS BETWEEN ROWS AND NOT UNDER EACH ONE, so the list does not end on a rule that
   * looks like a truncated section. */
  border-block-start: 1px solid var(--color-border-subtle);
}

.sw-fact-list__label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
  color: var(--color-text-secondary);
}

.sw-fact-list__value {
  margin: 0;
}

.sw-pill--invima {
  color: var(--color-status-warning-fg);
  background: var(--color-status-warning-bg);
}

/* `.sw-panel--source` WAS HERE — 2026-09-07. It spaced the product detail's Origen panel, and that
 * panel was removed whole at the client's request because the synchronised moment was its only row.
 * A modifier with no element is a rule nothing can be measured against. */

/* ---------------------------------------------------------------------------
   Product photograph — the detail's own image, NOT the table's thumbnail
   --------------------------------------------------------------------------- */

.sw-panel--photo {
  /* The image is the panel's whole content, so it is centred rather than left-aligned with the
   * text panels beside it — an off-centre photograph in a wide panel reads as a layout mistake. */
  display: flex;
  flex-direction: column;
  align-items: center;
}

.sw-product-photo {
  /* CONTAIN AND A CAP, NOT A FIXED SIZE. The catalogue photographs arrive at whatever dimensions
   * the store had them at; a fixed height would letterbox some and crop others. The cap is what
   * stops a large one dominating a dialog that also has to show the ficha.
   *
   * `auto` block-size with `contain` keeps the aspect ratio the photographer chose. */
  inline-size: 100%;
  max-inline-size: 20rem;
  block-size: auto;
  max-block-size: 20rem;
  object-fit: contain;

  background: var(--color-surface-image);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-2);
}

.sw-detail-list__prose {
  /* A MONOGRAPH SECTION IS A PARAGRAPH, and the short-value styling beside it is not built for one.
   * The measure is capped so a composition does not run the full width of a widened dialog, and the
   * wrapping rules are explicit because a chemical name has no spaces to break at. */
  max-inline-size: 60ch;
  overflow-wrap: break-word;
  white-space: pre-line;
}

/* ---------------------------------------------------------------------------
   Thumbnail — design-system §8.5, the fourth cell kind
   --------------------------------------------------------------------------- */

.sw-thumbnail {
  /* A FIXED SQUARE, AND THE SIZE IS NOT NEGOTIABLE PER CALLER. The photographs arrive at whatever
   * dimensions the store had them at; `object-fit: contain` on a fixed box is what makes a column of
   * them line up instead of stepping in and out. `contain` rather than `cover` because a medicine
   * box cropped to fill a square loses the part a reader identifies it by - the label. */
  display: inline-flex;
  align-items: center;
  justify-content: center;

  /* SQUARE FIRST, 3rem SECOND, AND THAT ORDER IS THE FIX. `inline-size: 3rem` with
   * `block-size: 3rem` is a square only while the column is wide enough; base.css caps every img at
   * `max-width: 100%`, so in a narrower column the cap won on the inline axis, the block size did
   * not move, and the box became a tall rectangle. Measured on /commercial/catalog: 22x48 at 1024
   * and 45x48 at 1280 - so it was already out of square one breakpoint above where anybody had
   * looked. Question 124.
   *
   * `min()` states the cap the browser was applying anyway, and `aspect-ratio` makes the other axis
   * follow it. The box now shrinks as a square - 38px at 1024, a full 48 from 1280 up - instead of
   * distorting. `object-fit: contain` was never the problem: the PICTURE inside was letterboxed
   * correctly the whole time, and what looked wrong was the bordered box around it. */
  inline-size: min(3rem, 100%);
  aspect-ratio: 1;
  block-size: auto;
  object-fit: contain;
  background: var(--color-surface-image);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  padding: 2px;
}

.sw-thumbnail--empty {
  /* The placeholder is the same box, so a product with no photograph does not shorten its row and a
   * page of ten stays on one rhythm whether or not every image resolved. */
  color: var(--color-text-disabled);
}

/* The empty state is one region across the card, not a value beside a label. */
.sw-table__row--empty {
  display: block;
  padding: 0;
  border: 0;
  background: none;
}

.sw-table__cell--empty {
  display: block;
  grid-column: 1 / -1;
}

/* ---- Skeleton ---- */

.sw-table__row--skeleton .sw-table__cell {
  display: block;
}

.sw-skeleton {
  display: block;

  block-size: 0.75rem;
  border-radius: var(--radius-sm);

  background-image: linear-gradient(
    90deg,
    var(--color-skeleton-base) 25%,
    var(--color-skeleton-sheen) 37%,
    var(--color-skeleton-base) 63%
  );
  background-size: 400% 100%;

  animation: sw-skeleton-shimmer var(--sw-skeleton-duration, 1.4s) ease-in-out infinite;
}

@keyframes sw-skeleton-shimmer {
  from { background-position: 100% 50%; }
  to   { background-position: 0 50%; }
}

/* §9: motion is opt-out. The placeholder still reads as a placeholder without travelling. */
@media (prefers-reduced-motion: reduce) {
  .sw-skeleton {
    animation: none;
  }
}

/* ---- Empty state ---- */

.sw-empty-state {
  display: grid;
  justify-items: center;
  gap: var(--space-3);

  padding-block: var(--space-6);
  padding-inline: var(--space-4);

  text-align: center;
}

.sw-empty-state__icon {
  inline-size: 2rem;
  block-size: 2rem;
  color: var(--color-text-secondary);
}

.sw-empty-state__message {
  margin: 0;
  color: var(--color-text-secondary);
}

/* ===================================================================================
 * `md` and up: the real table.
 *
 * Every display value here is the element's own default, restored. The base rules above take the
 * table apart for the card layout; these put it back. `table-layout: fixed` is the one addition,
 * and it is what makes §8.5's "skeleton rows matching the final column widths" true rather than
 * approximately true - with `auto`, column widths are measured from content, so a placeholder row
 * and a data row produce different columns and the layout jumps at exactly the moment the
 * requirement is about.
 * =================================================================================== */

@media (min-width: 768px) {
  .sw-table {
    display: table;
    table-layout: fixed;
    border-collapse: collapse;
  }

  /* THE PROPORTIONS, ON A CLOSED TWELVE-UNIT SCALE. §8.5, ADR-0038, open question 67.
   *
   * A class rather than a `style` attribute, because ADR-0006's policy refuses every inline style
   * and no nonce covers an attribute - which is what made `TableColumn.Width` a dead feature from
   * the day it was written. Every value the scale can take has a rule here, so a span is either one
   * of these twelve or it is refused by `DataTableModel` before it renders.
   *
   * ALL TWELVE ARE DECLARED THOUGH FOUR ARE USED. The scale is the mechanism; a table needing a
   * three-twelfths column should not also need a stylesheet edit, and a partial scale is how the
   * next author concludes the numbers are arbitrary. */
  .sw-table__colgroup { display: table-column-group; }
  .sw-table__col      { display: table-column; }

  .sw-table__col--1  { inline-size: 8.3333%; }
  .sw-table__col--2  { inline-size: 16.6667%; }
  .sw-table__col--3  { inline-size: 25%; }
  .sw-table__col--4  { inline-size: 33.3333%; }
  .sw-table__col--5  { inline-size: 41.6667%; }
  .sw-table__col--6  { inline-size: 50%; }
  .sw-table__col--7  { inline-size: 58.3333%; }
  .sw-table__col--8  { inline-size: 66.6667%; }
  .sw-table__col--9  { inline-size: 75%; }
  .sw-table__col--10 { inline-size: 83.3333%; }
  .sw-table__col--11 { inline-size: 91.6667%; }
  .sw-table__col--12 { inline-size: 100%; }

  .sw-table__caption  { display: table-caption; margin-block-end: var(--space-2); }
  .sw-table__head     { display: table-header-group; }
  .sw-table__body     { display: table-row-group; }
  .sw-table__head-row { display: table-row; }
  .sw-table__row      { display: table-row; }

  .sw-table__header,
  .sw-table__cell {
    display: table-cell;

    padding: var(--space-3) var(--space-4);
    vertical-align: middle;
  }

  /* Undoes the clipping above, field by field: `display` alone would leave a 1px absolutely
   * positioned header sitting outside the table.
   *
   * FIELD BY FIELD IS THE HAZARD, AND IT SHIPPED. This rule listed seven properties and the base
   * one sets nine - `padding: 0` and `border: 0` were left behind, and they are not neutral
   * values. A media query adds no specificity, so `.sw-table__header, .sw-table__cell { padding }`
   * at (0,1,0) and `.sw-table__header { border-block-end }` at (0,1,0) both LOSE to the base rule
   * at (0,2,0) - `:not()` and `:has()` each take their argument's specificity. So every header
   * with no sort control kept zero padding and no separator at every width.
   *
   * On screen: `Cupo de crédito` is right-aligned and `Estado` is left-aligned, so with no padding
   * between them the two labels touched - "Cupo de créditoEstado" - while the six affected headers
   * sat 1rem out of line with the data beneath them, which keep `.sw-table__cell`'s padding. Only
   * `Nombre` looked right, because it is the one header carrying a sort button.
   *
   * §8.5's header surface and 1px separator are the specification; this is what makes them true
   * for the whole header row rather than for the sortable part of it. */
  .sw-table__header:not(:has(.sw-table__sort)) {
    position: static;
    width: auto;
    height: auto;
    padding: var(--space-3) var(--space-4);
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    border-block-end: 1px solid var(--color-border-subtle);
  }

  /* THE PICTURE CELL STOPS PAYING TEXT PADDING. Every cell carries `var(--space-4)` each side so
   * words do not touch a column rule; a thumbnail carries its own 1px border, its own 2px inset and
   * its own radius, so it is already separated from what is beside it. At 1024 that padding was the
   * difference between a 48px square and a 22px sliver - the column is 54px, and 32 of it went to
   * space the picture did not need.
   *
   * DOUBLED CLASS FOR SPECIFICITY, for the reason two rules up. The base padding is set by
   * `.sw-table__header, .sw-table__cell` at (0,1,0); this at (0,1,0) would win only by source order,
   * and source order is exactly what the next edit to this file reshuffles. */
  .sw-table__cell.sw-table__cell--image {
    padding-inline: var(--space-2);
  }

  .sw-table__header {
    background: var(--color-surface-secondary);
    border-block-end: 1px solid var(--color-border-subtle);

    /* A TABLE HEAD RATHER THAN A ROW OF BOLD DATA. The colour and the weight were already §8.5's;
     * what was missing is that the heading was set at the same size as the values under it, so the
     * head read as a first row rather than as a label for the rest. Smaller and wider-tracked is
     * the whole difference, and both values are existing tokens.
     *
     * IN THE `md` QUERY ONLY, deliberately. Below it these same elements are the card layout's sort
     * CONTROLS, not headings - tracking out the label on a button is a different decision nobody
     * has taken. */
    font-size: var(--font-size-sm);
    letter-spacing: var(--letter-spacing-wide);
  }

  /* The action column shifts to the trailing edge at this width; its heading has to go with it, or
   * "Acciones" sits a column away from the controls it names. Same rule `--numeric` has always
   * had, for the other column that does not start at the leading edge. */
  .sw-table__header--action {
    text-align: end;
  }

  /* ONE RHYTHM WHATEVER A ROW HOLDS. A row carrying a badge is taller than one whose cells are all
   * placeholders, so an eye running down a list of forty customers meets a line that stutters
   * wherever the data thins out. The floor is the badge row's own height, so nothing is pushed
   * taller and only the short rows move. */
  .sw-table__row {
    min-block-size: var(--space-12);
  }

  /* The control fills its header rather than sitting inside it as a chip: at this width the header
   * cell IS the target, and a bordered button inside a shaded header is two boxes saying one
   * thing. */
  /* NOTE WHAT IS NOT RESET HERE: min-height. §8.5's table layout starts at `md` (768px) and §9
   * rule 6's 44x44 touch minimum runs until `lg` (1024px), so between those two the table looks
   * like a desktop table and is still being used on a touch screen. Resetting the height here is
   * what the first version did, and the probe measured the sort controls at 112x21 at 768px - a
   * violation in a 256px-wide band that no phone width and no desktop width would have shown. */
  .sw-table__sort {
    inline-size: 100%;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    text-align: start;
  }

  .sw-table__sort:hover {
    background: none;
    color: var(--color-text-primary);
  }

  .sw-table__row {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    border-block-end: 1px solid var(--color-border-subtle);
  }

  .sw-table__row:hover {
    background: var(--color-surface-hover);
  }

  .sw-table__row--selected,
  .sw-table__row--selected:hover {
    background: var(--color-surface-selected);
  }

  /* The per-cell labels exist for the card layout only. Here the headers are visible above the
   * data, and a label in every cell repeats each one down the page. */
  .sw-table__label {
    display: none;
  }

  .sw-table__header--numeric,
  .sw-table__cell--numeric {
    text-align: end;
  }

  .sw-table__header--numeric .sw-table__sort {
    justify-content: flex-end;
  }

  .sw-table__cell--identifier {
    font-weight: var(--font-weight-semibold);

    /* AN EMAIL ADDRESS HAS NO SPACES, SO NOTHING WRAPS IT AND IT DRAWS STRAIGHT OVER THE NEXT
     * COLUMN. Found by looking at /admin/users in the v2 spike: the identifier ran across the Roles
     * cell and the two strings overlapped, which reads as a rendering fault rather than as
     * overflow. `table-layout: fixed` is what makes it visible - the column cannot grow to fit - and
     * that is the right trade, because the alternative is columns that jump width with the data.
     *
     * `anywhere` rather than `break-word`: break-word only breaks where a normal break opportunity
     * would have existed, and a long address offers none. */
    overflow-wrap: anywhere;
  }

  .sw-table__cell--status {
    justify-self: auto;
  }

  /* The action column sits at the trailing edge, where a reader who has finished reading the row
   * arrives anyway. */
  .sw-table__cell--action {
    text-align: end;
  }

  /* TWO CONTROLS IN ONE CELL, LAID OUT RATHER THAN LEFT TO INLINE FLOW — added 2026-09-06 with the
   * cartera action's return.
   *
   * A COMMENT IN _DataTable.cshtml SAID THIS CELL WAS "already a flex row with a gap" AND IT WAS
   * NOT: `.sw-table__cell--action` is `display: block` in the base rule and `table-cell` in this
   * query, never flex. So the only thing between the two anchors was the whitespace Razor leaves
   * between them in the loop, and the only thing placing them was `text-align: end` over two
   * inline-flex boxes. That is the layout that wrapped.
   *
   * THE VALUE SPAN CARRIES IT, NOT THE CELL. A <td> given `display: flex` stops being a table cell,
   * and `table-layout: fixed` then has nothing to size — the twelfths above would silently stop
   * applying to this column. The span is already in the markup for every cell kind, so this costs
   * no element.
   *
   * `nowrap` IS THE ASSERTION, NOT A PREFERENCE. The column is cut to hold both controls; if a
   * future label outgrows it the right answer is another twelfth or a shorter word, and wrapping
   * would hide that by growing the row instead — which is exactly how the fault this replaces
   * reached the published site. */
  .sw-table__cell--action .sw-table__value {
    display: flex;
    flex-wrap: nowrap;
    justify-content: flex-end;
    gap: var(--space-2);
  }

  /* AND THE LABEL DOES NOT WRAP INSIDE THE CONTROL, WHICH IS A SECOND WAY TO GROW THE ROW AND WAS
   * INVISIBLE TO THE FIRST VERSION OF THE TEST THAT GUARDS THIS.
   *
   * With the pair laid out on one line, a flex item still shrinks to its MIN-CONTENT width, and the
   * min-content width of a TWO-WORD label is its longer word - so the control fitted the column by
   * breaking its own label in two. Measured at 1280 on the label this rule was written for, which
   * was two words until 2026-09-07: both controls shared a top edge, the geometry assertion passed,
   * and the row was 77px instead of 56px. A two-line label inside a small outlined control is the
   * same fault the column widening exists to remove, arriving one level further in.
   *
   * THE WORKED EXAMPLE IS GONE AND THE RULE IS NOT. Every row-action label on the surfaces this
   * governs is now a single word, whose min-content width is the whole word - so none of them can be
   * broken at a space any more. What `nowrap` still prevents is a single word being broken by an
   * overflow-wrap or a narrower column, and the day a multi-word action label returns it is the only
   * thing standing between that label and a taller row.
   *
   * `nowrap` MAKES THE REQUIREMENT HONEST. The control's width becomes a fixed number the column
   * either has room for or does not, so an undersized column overflows visibly and is caught,
   * instead of being absorbed into a taller row nobody declared. */
  .sw-table__action {
    white-space: nowrap;
  }

  /* THE OUTLINE STAYS AT EVERY WIDTH, AND THIS REVERSES A DECISION RATHER THAN REFINING ONE.
   * ADR-0026 dropped the border here and made the desktop action a plain underlined link, on the
   * argument that "twenty outlined buttons down a desktop table is a column of boxes shouting over
   * the data they belong to". The product owner has looked at the result and asked for the opposite:
   * the action read as incidental text on a surface whose whole purpose is to be acted on.
   *
   * OUTLINED, NOT FILLED, and that is the part the emphasis hierarchy decides rather than taste.
   * §8.1 gives one primary control per view; a control repeated on every one of 950 rows cannot be
   * the emphatic thing on the page, and 950 filled buttons would out-shout whatever is. So: a
   * subtle border, no background at rest, the brand green carrying the glyph and the label, and a
   * tonal fill that arrives only on hover and focus.
   *
   * IDENTICAL IN EVERY ROW. No hover-to-reveal - a control that appears on hover does not exist for
   * touch, for a keyboard reader scanning, or for anybody who has not moved the pointer.
   *
   * min-block-size is NOT reset here: §9 rule 6 runs until lg, and the band between md and lg is a
   * table being used on a touch screen. That is the exact gap the sort control's 112x21 measurement
   * came from. */
  .sw-table__action {
    display: inline-flex;

    /* Tighter than the card's, because at this width the control sits inside a table cell rather
     * than filling the foot of a card. */
    padding-block: var(--space-1);
    padding-inline: var(--space-3);
  }

  .sw-table__row--empty,
  .sw-table__row--empty:hover {
    display: table-row;
    background: none;
  }

  .sw-table__cell--empty {
    display: table-cell;
  }
}

/* ===================================================================================
 * `lg` and up: the pointer is a mouse, so the touch minimum stops applying.
 *
 * §9 rule 6 is scoped "below the lg breakpoint" and this is the only place in the component that
 * needs to know it. Kept as its own block rather than folded into the `md` one, because those two
 * breakpoints answer different questions - `md` is when the layout becomes a table, `lg` is when
 * the input stops being a finger - and merging them is what put a 21px tap target on a tablet.
 * =================================================================================== */

@media (min-width: 1024px) {
  .sw-table__sort {
    min-height: 0;
  }

  .sw-table__action {
    min-block-size: 0;
  }
}

/* ===================================================================================
 * Filter bar and chips — §8.3
 *
 * Every value below is §8.3's: --color-primary-50 background, 1px --color-action-primary border,
 * --color-action-primary text, --radius-full. Nothing here is chosen.
 *
 * THE TOUCH TARGET IS THE DISMISS BUTTON, NOT THE CHIP. §9 rule 6 wants 44x44 below `lg` for
 * anything a finger has to hit; the chip itself is a pill a good deal smaller than that and is
 * correct at that size, because it is not the control. Confusing the visible control with the
 * touch target is what put a 21px box on the data table's sort buttons, so the minimum is stated
 * on the button and measured by the probe rather than assumed from the pill's padding.
 * =================================================================================== */

.sw-filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);

  margin-block-end: var(--space-4);
}

.sw-filter-bar__chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);

  margin: 0;
  padding: 0;
  list-style: none;
}

/* A <ul> of chips rather than a row of spans: it is a list of applied filters, and a screen
 * reader announcing "list, 3 items" before them is the count a sighted reader gets for free. */
.sw-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);

  padding-block: 2px;
  padding-inline-start: var(--space-3);

  border: 1px solid var(--color-action-primary);
  border-radius: var(--radius-full);

  /* §8.3 NAMES --color-primary-50, WHICH IS A LAYER 1 PRIMITIVE, and §1 forbids a component from
   * reaching past the semantic layer. DesignTokenLayerTests failed the build on it rather than
   * anyone noticing in review.
   *
   * The two rules turn out not to conflict: --color-surface-selected IS --color-primary-50, so
   * the specified colour is reachable through Layer 2 - and it is the better name anyway. An
   * applied filter is a selection, which is what the alias means; the primitive only says which
   * green. Same for the hover below, where --color-action-primary-hover is --color-primary-700. */
  background: var(--color-surface-selected);

  /* --color-tone-brand-fg RATHER THAN --color-action-primary SINCE V2, AND THE DARK THEME IS WHY.
   * axe measured this pair at 4.35:1 with the operating system in dark - a real failure, found by
   * the smoke check rather than by reading.
   *
   * The reason is a semantic one rather than an arithmetic accident. On dark, --color-action-primary
   * is a FILL: a light green chosen to clear 3:1 as a button's shape while carrying dark text. Using
   * a fill colour as TEXT on a dark tint is asking one token to do two jobs, and it only worked on
   * light because there the fill happens to be a text-safe green. --color-tone-brand-fg is the token
   * for brand-coloured TEXT on a brand-tinted surface, and it resolves correctly in both.
   *
   * The border keeps --color-action-primary: it is a boundary, SC 1.4.11 wants 3:1 rather than
   * 4.5:1, and it measures well clear of that on both themes. */
  color: var(--color-tone-brand-fg);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
}

.sw-chip__label {
  white-space: nowrap;
}

.sw-chip__dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  /* §9 rule 6, relaxed at `lg` where the pointer stops being a finger - the same two-breakpoint
   * split the data table needed, kept explicit here rather than inherited by accident. */
  min-inline-size: 44px;
  min-block-size: 44px;

  padding: 0;
  border: 0;
  background: none;

  color: inherit;
  cursor: pointer;
}

.sw-chip__dismiss:hover {
  color: var(--color-action-primary-hover);
}

/* The glyph is small even though its target is not. Sized here rather than by .sw-icon's default,
 * because a 20px cross inside a pill this size reads as a second element rather than a control. */
.sw-chip__dismiss-icon {
  inline-size: var(--icon-size-sm);
  block-size: var(--icon-size-sm);
}

/* ===================================================================================
 * KPI card — §8.8
 *
 * --color-surface-primary, --radius-lg, --shadow-sm; label at --font-size-sm in
 * --color-text-secondary; figure at --font-size-3xl in the display face with tabular-nums.
 *
 * THE DELTA'S DIRECTION IS AN ARROW AND A WORD, NOT A COLOUR. §8.8 gives the reason: colour alone
 * never encodes up or down, and about one man in twelve cannot rely on it. The colour below is
 * REINFORCEMENT - deleting it must leave the direction legible, which it does, because the arrow
 * and the visually hidden word both survive.
 * =================================================================================== */

.sw-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);

  padding: var(--space-5);

  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  background: var(--color-surface-primary);
  box-shadow: var(--shadow-sm);

  transition:
    box-shadow var(--duration-base) var(--easing-standard),
    transform var(--duration-base) var(--easing-standard);
}

/* A CARD LIFTS ON HOVER AND THAT IS THE WHOLE MOTION BUDGET FOR IT. Elevation is §8.1's hover
 * affordance for buttons; the same idea one step gentler is what makes a grid of cards feel like
 * objects rather than printed rectangles. */
.sw-kpi:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

/* ---- The icon chip — NEW IN V2 ----
 *
 * §8.8 lists surface, radius, shadow, label, figure, delta and sparkline, and no icon. §11 item 17
 * recorded the disagreement: --icon-size-lg's own comment in §7 says "primary entry points, KPI
 * cards", and the brand mockup draws a circled glyph at the top right of each card. V2 builds it,
 * on the LEADING edge rather than trailing, because a row of cards reads left to right and the chip
 * is what distinguishes one card from the next at a glance.
 *
 * IT CARRIES A TONE, AND THE TONES ARE NOT STATUS. --color-tone-brand / -teal / -amber exist so a
 * dashboard of four cards is not four green circles. Nothing here means "in trouble"; a card that
 * needs to say that has a delta with a status colour and an arrow.
 *
 * A LUCIDE GLYPH FROM THE INLINE SPRITE - §7 - and never an emoji: an emoji is a font-dependent
 * picture we do not control, it cannot inherit currentColor, and it announces as its own name to a
 * screen reader. The chip is aria-hidden; the card's meaning is in its label. */
.sw-kpi__chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: flex-start;

  inline-size: 40px;
  block-size: 40px;
  margin-block-end: var(--space-1);

  border-radius: var(--radius-md);
}

.sw-kpi__chip .sw-icon {
  inline-size: var(--icon-size-lg);
  block-size: var(--icon-size-lg);
}

.sw-kpi__chip--brand { background: var(--color-tone-brand-bg); color: var(--color-tone-brand-fg); }
.sw-kpi__chip--teal  { background: var(--color-tone-teal-bg);  color: var(--color-tone-teal-fg); }
.sw-kpi__chip--amber { background: var(--color-tone-amber-bg); color: var(--color-tone-amber-fg); }

/* Uppercase, small, and opened up - §5's --letter-spacing-wide, which exists for exactly this. A
 * KPI label is a caption rather than a sentence, and setting it as one is most of what separates a
 * dashboard that looks designed from one that looks assembled. */
.sw-kpi__label {
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
}

.sw-kpi__figure {
  margin: 0;

  color: var(--color-text-primary);
  font-family: var(--font-family-display);
  font-size: var(--font-size-3xl);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-tight);

  /* §10 rule 4. Four cards side by side are read as a row, and digits that do not align across
   * them make the row harder to scan than four separate numbers would be. */
  font-variant-numeric: tabular-nums;
}

.sw-kpi__delta {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-wrap: wrap;

  margin: 0;
  font-size: var(--font-size-sm);
}

.sw-kpi__arrow {
  inline-size: var(--icon-size-sm);
  block-size: var(--icon-size-sm);
}

.sw-kpi__delta-amount {
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
}

/* The basis is context, not the number. §8.8 requires it to be present - "12,5%" against nothing
 * is not a comparison - and it recedes so the figure above stays the thing being read. */
.sw-kpi__delta-basis {
  color: var(--color-text-secondary);
}

/* Reinforcement only. Every one of these pairs with an arrow and a spoken word. */
.sw-kpi__delta--success  { color: var(--color-status-success-fg); }
.sw-kpi__delta--error    { color: var(--color-status-error-fg); }
.sw-kpi__delta--warning  { color: var(--color-status-warning-fg); }
.sw-kpi__delta--info     { color: var(--color-status-info-fg); }
.sw-kpi__delta--pending  { color: var(--color-status-pending-fg); }
.sw-kpi__delta--inactive { color: var(--color-status-inactive-fg); }

.sw-kpi__sparkline {
  inline-size: 100%;
  block-size: 32px;

  /* currentColor so the line takes the delta's tone from the card, and no fill: a filled polyline
   * closes itself into a wedge. */
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;

  color: var(--color-action-primary);
}

/* ===================================================================================
 * KPI grid — §6 breakpoints, and the count of cards it is given
 *
 * Stacked below `md`, 2-up at `md`, 4-up at `xl`. The table in §6 is the whole specification of the
 * STEPS, and the three rules below are it.
 *
 * THE STEPS ALONE LEAVE A HOLE FOR EVERY COUNT THAT IS NOT A MULTIPLE OF THE COLUMNS - measured
 * 2026-09-09, and worse than it was reported. At `md` the clean counts are 1, 2, 4 and 6; at `xl`
 * only 4 and 8. So three cards left a quarter of the row empty at 1280 and half the second row empty
 * at 768 - and TWO cards left half the row empty at 1280, which is on the design gallery's own
 * second example and has been since the component shipped.
 *
 * IT IS THE COMPONENT'S FAULT AND NOT THE SURFACE'S, which is why the fix is here: the next page
 * with three or five cards inherits the same hole, and a caller cannot be expected to know it.
 * =================================================================================== */

.sw-kpi-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);

  margin-block-end: var(--space-6);
}

@media (min-width: 768px) {
  .sw-kpi-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1280px) {
  .sw-kpi-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* ---- The count rules: a quantity query per count, 1 to 4 ----
 *
 * WHY NOT auto-fit WITH A minmax FLOOR, WHICH IS THE OBVIOUS ANSWER. It fixes three and BREAKS
 * FOUR, and that was measured rather than argued: with repeat(auto-fit, minmax(14rem, 1fr)) here the
 * four-card header drew TWO ROWS WITH 501px OF THE SECOND ONE EMPTY at 768. auto-fit fits as many
 * tracks as the floor allows and has no way to know the count it was given, so between roughly 700px
 * and 940px of content that number is THREE - and the defect moves onto Inicio and /admin/users, the
 * two surfaces that are correct today. KpiGridLayoutTests is where that measurement lives.
 *
 * WHY NOT A MODIFIER CLASS THE CALLER PASSES. /admin/users writes `class="sw-kpi-grid"` by hand on
 * a <section> so it can carry an aria-label, and does not go through _KpiGrid at all. A modifier
 * emitted by the partial would fix every caller except that one - CLAUDE.md §7's assertion scoped
 * where the failure cannot occur, in a stylesheet - and a rule whose correctness depends on somebody
 * remembering to pass it is not a control.
 *
 * SO THE STYLESHEET COUNTS THE CHILDREN ITSELF. `:has(> :nth-child(N):last-child)` is exactly N
 * children: the Nth is also the last. :has() is already used nine times in this file, so this
 * introduces no new browser requirement.
 *
 * THE SCALE STOPS AT FOUR, AND THE FLOOR IS THE REASON RATHER THAN TASTE. Five columns at 1280
 * would be 188px a card, below the 234px the product already ships at its tightest - and a card
 * narrower than its figure is worse than a hole. Five or more cards therefore keep the four-column
 * step and can still hole; §8.8 says so, and the answer for a surface that needs it is another rule
 * here rather than a floor nobody measured.
 *
 * THEY SIT AFTER BOTH STEPS AND INSIDE THE md QUERY, so source order and specificity agree. Each
 * carries (0,3,0) against the steps' (0,1,0), which is what lets one declaration hold from 768px
 * upwards without being restated in the 1280 block - and restating it there would be a second place
 * to change. Below md nothing is needed: one column has no hole at any count. */
@media (min-width: 768px) {
  .sw-kpi-grid:has(> :first-child:last-child) {
    grid-template-columns: 1fr;
  }

  /* TWO IS REDUNDANT AT md AND LOAD-BEARING AT xl. The step above already gives two columns here;
   * without this rule the 1280 step would give two cards four columns and half an empty row. */
  .sw-kpi-grid:has(> :nth-child(2):last-child) {
    grid-template-columns: repeat(2, 1fr);
  }

  .sw-kpi-grid:has(> :nth-child(3):last-child) {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 1024px) {
  .sw-chip__dismiss {
    min-inline-size: 0;
    min-block-size: 0;
    padding-inline: var(--space-2);
  }
}

/* ===================================================================================
 * Select — §8.3
 *
 * THE NATIVE ELEMENT IS STYLED, NOT REPLACED. §8.10 records this rule for the checkbox; the
 * select is where it costs the most to break. A div-based listbox loses keyboard navigation and
 * type-ahead, loses form participation, and - the decisive one - loses the operating system's own
 * picker on a phone: a wheel on iOS, a dialog on Android, both sized for a thumb. A custom listbox
 * renders a cramped in-page dropdown instead, and Novofarlab's salespeople are on phones.
 *
 * THE BOX COMES FROM .sw-field__control AND IS NOT REDECLARED HERE. That rule is class-scoped
 * rather than element-scoped, so a <select> carrying the class already gets §8.2's border, radius,
 * 44px minimum, 16px font and focus transition. Only what a select needs BEYOND a text field is
 * below - which is the arrow and the room for it.
 *
 * WHAT IS DELIBERATELY ABSENT: appearance: base-select, and the reason is this project's rule
 * rather than caution. As of mid-2026 it is Chromium-only, behind a flag in Firefox and in preview
 * in Safari - not Baseline - so shipping it would produce TWO visual states of one component, and
 * the gallery can only render the one the machine running it supports. The other would be styled,
 * shipped and never seen in a browser, which is exactly the "CSS only" state that re-marking
 * D-c's table just eliminated. Revisit when it reaches Baseline; see §8.3.
 * =================================================================================== */

.sw-select {
  position: relative;
}

.sw-select__control {
  /* Strips the platform's own arrow and background so ours can be drawn. -webkit- first for
   * older WebKit, which ignores the unprefixed property on a select. */
  -webkit-appearance: none;
  appearance: none;

  /* Room for the arrow, expressed in the same units the arrow is, so a long option's text never
   * runs underneath it at any zoom - the same reservation .sw-field__row--with-action makes for
   * the show-password toggle. */
  padding-inline-end: calc(var(--space-4) + var(--icon-size-md) + var(--space-3));

  /* A select with a long option otherwise widens its own column past the container. */
  max-inline-size: 100%;
  text-overflow: ellipsis;
}

.sw-select__arrow {
  position: absolute;
  inset-inline-end: var(--space-4);
  inset-block-start: 50%;
  transform: translateY(-50%);

  color: var(--color-text-secondary);

  /* A click on the glyph still opens the list, because the event passes through to the control
   * underneath. Without this the arrow is a dead zone in the middle of the target. */
  pointer-events: none;
}

.sw-select__control:disabled + .sw-select__arrow {
  color: var(--color-text-disabled);
}

/* ---- The detail list — label/value pairs inside a panel ----
 *
 * A REAL <dl>, because that is what a set of term/definition pairs is, and a screen reader
 * announces it as a description list with a count. A grid of divs would look identical and carry
 * none of that.
 *
 * The row is a grid rather than a flex pair so the values line up down the panel: a label column
 * sized to its content at md and above, and stacked below it, because a 320px viewport has no room
 * for two columns of Spanish. --space-1 between term and value, --space-3 between rows.
 */
.sw-detail-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  margin: 0;
}

.sw-detail-list__row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.sw-detail-list__row dt {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

.sw-detail-list__row dd {
  margin: 0;
  color: var(--color-text-primary);

  /* Figures line up across rows, for the same reason §8.5 asks for it in a numeric column. */
  font-variant-numeric: tabular-nums;

  /* A LAST RESORT, AND ONLY AT A SENSIBLE BOUNDARY. `break-word` breaks inside a word only when the
   * line offers no other opportunity, so an identification or a territory list wraps between its
   * parts rather than mid-token. NOT `anywhere`, which §8.5 uses for an email address in a fixed
   * table column: there the alternative is text drawn over the next cell, and here the value has the
   * panel's full width and nothing to collide with. */
  overflow-wrap: break-word;
}

/* A MOMENT IS ONE TOKEN, and it does not read as one if the line breaks inside it. The Spanish
 * short format is "12/08/2026, 4:02 p. m." - four space-separated parts, so a narrow column breaks
 * it four ways and the meridiem ends up on its own line as "m.". That was measured in the dialog
 * before the container query above existed, and this is the guard that keeps it true if a future
 * layout squeezes the value again.
 *
 * Safe to declare unconditionally: the value column is never narrower than the panel's own content
 * box, and the longest moment this renders is about 21 characters.
 *
 * AND IT IS NOT CURRENTLY RED-CHECKABLE, WHICH IS WORTH KNOWING BEFORE SOMEBODY DELETES IT AS DEAD.
 * Measured 2026-08-14: with the container query above in place the value gets the panel's full width
 * in the dialog, so removing this rule leaves the moment on one line anyway and
 * `CustomerDetailLayoutTests` stays green. It is belt-and-braces for the case the query does not
 * cover - a genuinely narrow container, or a locale whose meridiem carries an internal space.
 *
 * That last one is not hypothetical: this was reported from a desktop rendering "12/08/2026, 4:02
 * p. m.", which breaks four ways. The test host's ICU renders "14/08/2026 7:59 a.m." with no space
 * in the meridiem, so it can only break two ways and the four-line case cannot be reproduced here at
 * all. The rule covers a format this machine does not produce.
 *
 * AND SINCE 2026-09-07 IT HAS NO ELEMENT TO APPLY TO. The product detail's Origen panel was the last
 * `.sw-detail-list__row` carrying a `<time>`, and it was removed at the client's request. The rule
 * is KEPT rather than deleted, and that is a different judgement from the one made about
 * `.sw-panel--source` in the same commit: that modifier described a specific block that is gone,
 * while this is a defensive property of a general-purpose list any future dated row would want. The
 * paragraph above already says it is not red-checkable; it is now not reachable either, which is
 * the state to know before treating a green suite as evidence for it. */
.sw-detail-list__row time {
  white-space: nowrap;
}

/* ---- A detail list that goes two across ----
 *
 * FOUR SHORT VALUES IN ONE COLUMN IS HALF A DIALOG OF WHITE SPACE BESIDE A COLUMN OF DASHES. The
 * customer's Contacto section is exactly that: e-mail, móvil, fijo, dirección, and two of the four
 * are a dash for every customer today because the phone bridge does not exist. Stacked, it is 232px
 * of a 654px dialog body; two across it is 116px, which is the difference between a desktop reader
 * seeing the whole customer and scrolling inside an overlay.
 *
 * 40rem, THE SAME THRESHOLD THE PANELS AND THE HERO SHARE, and sharing it is the decision. One
 * number decides whether this partial is "wide" and every region answers it together; a second
 * threshold would give a container width at which the list had split and the hero had not, which
 * reads as a rendering fault.
 *
 * IT SITS AFTER `.sw-detail-list`, AND THAT IS LOAD-BEARING RATHER THAN TIDINESS — THIS RULE WAS
 * WRITTEN 2000 LINES ABOVE FIRST AND DID NOTHING AT ALL. Both selectors are one class, so
 * specificity ties and SOURCE ORDER decides: `display: flex` won, the list stayed one column, and
 * there was no error, no warning and nothing to see except a dialog that still scrolled. It was
 * found by measuring the panel and getting 232px where 116px was expected. The same trap
 * `.sw-dialog--reading` and `.sw-hero--identity` each record for themselves — three times now, in
 * one stylesheet.
 *
 * COLUMN GAP ONLY. The row gap is `.sw-detail-list`'s own --space-3 and stays there; declaring both
 * here would make this rule the place two different spacings are decided.
 *
 * A MODIFIER RATHER THAN THE DEFAULT, because the product ficha's rows are prose and a two-column
 * paragraph in a dialog is worse than a long one. */
@container sw-detail (min-width: 40rem) {
  .sw-detail-list--split {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-6);
  }
}

/* `.sw-detail-list__row`'s label-beside-value grid USED TO BE HERE, keyed on the viewport, which is
 * how a 32rem dialog on a 1440px screen came to hand each value a 4rem column. It moved to a
 * container query beside `.sw-detail`. */

/* ===================================================================================
 * The order-taking surface — ADR-0048 (the draft) and ADR-0049 (the pickers)
 *
 * COMPOSED FROM WHAT §8 ALREADY DEFINES. There is no new component here: the steps are headings,
 * the pickers are a search field plus a list of buttons, the lines are a table, and the summary is
 * a definition list. §11 defines no stepper and no combobox, and a wizard that hides state is worse
 * than a page that shows it.
 * =================================================================================== */

/* CONTAINER QUERY, NOT A MEDIA QUERY, for the same reason `.sw-detail` uses one: the surface has to
 * answer to the space IT is given. The same partial is correct on a full page and would be correct
 * in a narrower shell without a second breakpoint being invented for it. */
.sw-order {
  container-type: inline-size;
  container-name: sw-order;
}

.sw-order__columns {
  display: grid;
  gap: var(--space-6);
}

/* 60rem, and it is derived rather than chosen: the lines table needs about 36rem before its four
 * columns start wrapping their headings, and the picker needs about 20rem before a product name and
 * its meta line collide. Below the sum of those two plus the gap, one column is the honest layout. */
@container sw-order (min-width: 60rem) {
  .sw-order__columns {
    grid-template-columns: minmax(0, 1fr) 22rem;
    align-items: start;
  }
}

.sw-order__main,
.sw-order__aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

/* THE PANEL HEADING. It used to carry a numbered badge; the number moved to the rail, which is the
 * only place on this surface where the sequence and the layout agree. What is left is the step's
 * own glyph beside its name - the SAME glyph the rail draws for that step, so the two cannot say
 * different things. */
.sw-order-step__title {
  display: flex;
  align-items: center;
  gap: var(--space-3);

  /* A HEADING ROW WITH A RULE UNDER IT, so a panel reads as a section rather than as text that
   * happens to be bigger. The margin is on the outside of the border and the padding inside it. */
  margin: 0 0 var(--space-4);
  padding-block-end: var(--space-3);
  border-block-end: 1px solid var(--color-border-subtle);

  font-family: var(--font-family-display);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
}

.sw-order-step__icon {
  flex: none;
  color: var(--color-action-primary);
}

/* THE PICKER: a list of buttons. Each row is a real submit control, so it is focusable, has a
 * visible focus ring from §9, and clears §9 rule 6's 44px target on a phone. */
.sw-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* AN IDENTITY ROW, ACROSS RATHER THAN DOWN. A mark, the name and its qualifier, whatever chips the
 * row carries, and an affordance - the same reading a customer row has anywhere else in the
 * product. It was a stacked name-over-meta, which is what made a list of eight of them read as a
 * wall of text with nothing to scan down. */
.sw-picker__choice {
  display: flex;
  flex-direction: row;
  align-items: center;

  /* IT WRAPS, AND THAT IS A MEASUREMENT RATHER THAN A PRECAUTION. At 320px a product row is a
   * 2.25rem mark, a name, a "1.000 disponibles" chip and an affordance, and the chip's longest
   * word does not break - measured at 350px against a 320px viewport, a 30px sideways scroll on
   * the whole page. Wrapping moves the chips to a second line on a phone and changes nothing from
   * `sm` up, where they fit. OrderNarrowViewportTests is what says so. */
  flex-wrap: wrap;
  gap: var(--space-3);
  inline-size: 100%;
  min-block-size: 2.75rem;
  padding: var(--space-3);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-primary);
  color: var(--color-text-primary);
  text-align: start;
  cursor: pointer;
}

/* THE TINTED SURFACE, not the plain hover grey: this row is about to become a selection, and
 * --color-surface-selected is the token that means exactly that in both themes. */
.sw-picker__choice:hover {
  border-color: var(--color-action-primary);
  background: var(--color-surface-selected);
}

/* The mark keeps its own size and never absorbs the row's free space. */
.sw-picker__mark {
  flex: none;
}

/* THE MONOGRAM IS THE HERO'S, AT THE ROW'S SCALE. `.sw-hero__monogram` carries the shape, the
 * family, the weight and the colour pair; only the geometry differs, because a 4rem disc on a
 * 2.75rem row is a different control. Overriding two lengths is what reuse looks like here -
 * a second monogram class would be two places for the colour pair to drift. */
.sw-picker__choice .sw-hero__monogram {
  inline-size: 2.25rem;
  block-size: 2.25rem;
  font-size: var(--font-size-sm);
}

/* The name and its qualifier are the one part that may shrink and wrap. */
.sw-picker__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* A basis rather than auto: with the row wrapping, `auto` lets a long product name claim the
   * whole line and push every chip onto its own. 8rem is enough to read a name by. */
  flex: 1 1 8rem;
  min-width: 0;
}

.sw-picker__name {
  font-weight: var(--font-weight-medium);
}

/* A PICKER ROW THAT IS A CHOICE RATHER THAN AN ACTION — the Bonificaciones dialog's product search, 2026-09-14.
 * The row is a <label> around a radio, so the whole 44px row selects; the chosen row carries the same surface and
 * border as a hovered one, and the radio itself says so to a reader who cannot see the tint. */
.sw-picker__radio {
  flex: none;
  inline-size: 1.25rem;
  block-size: 1.25rem;
  margin: 0;
  accent-color: var(--color-action-primary);
}

.sw-picker__choice:has(.sw-picker__radio:checked) {
  border-color: var(--color-action-primary);
  background: var(--color-surface-selected);
}

/* THE GROUP THOSE CHOICES SIT IN. A fieldset, so a screen reader announces the radios as one choice under its
 * legend; without its default border, padding and min-content width it reads as the order picker's list does. */
.sw-picker__group {
  margin: 0;
  padding: 0;
  border: 0;
  min-inline-size: 0;
}

/* A CHOICE ROW THAT IS A LINK — the Bonificaciones product search's matches, 2026-09-15. Choosing collapses the
 * list, and only an address does that with scripting off; the row keeps the picker's reading and loses a link's. */
a.sw-picker__choice {
  text-decoration: none;
}

/* THE CHOSEN PRODUCT, ALONE. It is not a control any more - "Cambiar" is - so it does not invite a click. */
.sw-picker__choice--chosen {
  cursor: default;
}

/* "CAMBIAR" GOES TO THE END OF THE ROW AND KEEPS THE 44PX TARGET the add button has; a button's own 40px is under
 * design-system.md §9 rule 6. */
.sw-picker__change {
  flex: none;
  min-block-size: 2.75rem;
  margin-inline-start: auto;
}

.sw-picker__meta {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE AFFORDANCE. Quiet at rest and brand-coloured under the pointer, so the row says it leads
 * somewhere without competing with the name. It is decoration: the button's accessible name
 * already says what the row does. */
.sw-picker__go {
  flex: none;
  color: var(--color-text-secondary);
}

.sw-picker__choice:hover .sw-picker__go {
  color: var(--color-action-primary);
}

/* THE CAP SENTENCE. Quiet, but present: it is a control rather than a decoration, so it is not
 * hidden behind a hover or a title attribute. */
.sw-picker__cap {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE CHOSEN CUSTOMER, once the search step is done. */
.sw-chosen {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-secondary);
  align-items: flex-start;
}

.sw-chosen__name {
  margin: 0;
  font-weight: var(--font-weight-semibold);
}

.sw-chosen__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

.sw-chosen__facts dt {
  font-weight: var(--font-weight-medium);
}

.sw-chosen__facts dd {
  margin: 0;
}

/* THE SUMMARY, ON ITS OWN SURFACE.
 *
 * It was a row of figures sitting directly on the panel, level with the button, so the total read
 * as one more number in a table. It is the figure somebody is about to commit a customer to. */
.sw-order__summary {
  margin-block-start: var(--space-5);
  padding: var(--space-5);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  background: var(--color-surface-secondary);
}

.sw-order__totals {
  display: flex;
  gap: var(--space-6);
  margin: 0;
}

.sw-order__totals dt {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

.sw-order__totals dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

/* THE TOTAL, ON ITS OWN LINE AND ABOVE THE CONFIRMING CONTROL, so the reading order is "this is
 * what it costs" and then "confirm" rather than the two competing side by side.
 *
 * THE FIGURE IS --font-size-2xl (24px) AND THE PROPOSAL SAID 26px. 26 is not on design-system.md
 * §6's type scale (20 / 24 / 30 / 36) and §6 wins over the proposal. 24 is the nearest value below,
 * which also keeps the total subordinate to the h1 exactly as the proposal's own 30-against-26
 * hierarchy does. */
.sw-order__paid {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin: var(--space-4) 0 0;
  padding-block-start: var(--space-4);
  border-block-start: 1px dashed var(--color-border-subtle);
}

.sw-order__grand dt {
  font-family: var(--font-family-display);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
}

.sw-order__grand dd {
  margin: 0;
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  color: var(--color-action-primary);
  font-variant-numeric: tabular-nums;

  /* tabular-nums is what stops the amount jumping sideways as the stepper changes it. The figure is
   * re-rendered by the server on every quantity change, so a proportional face would shift the one
   * number on the page a reader is watching. */
}

/* THE DOCUMENT'S BREAKDOWN - R-01. It sits under the order's own total, inside the same summary
 * surface, because it describes the same order and a second card would read as a second order.
 *
 * THE ROWS ARE A FLEX PAIR PER ROW rather than one wide flex line like .sw-order__totals above: the
 * values here are money of very different widths and three of them are whole sentences, so a label
 * on the left and a value on the right is what keeps the figures readable down one edge. */
.sw-order__breakdown {
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-4);
  border-block-start: 1px dashed var(--color-border-subtle);
}

.sw-order__breakdown-title {
  margin: 0 0 var(--space-3);
  font-family: var(--font-family-display);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.sw-order__breakdown-rows {
  margin: 0;
}

.sw-order__breakdown-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-2);
}

.sw-order__breakdown-row + .sw-order__breakdown-row {
  border-block-start: 1px solid var(--color-border-subtle);
}

.sw-order__breakdown-label {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

.sw-order__breakdown-value {
  margin: 0;
  font-variant-numeric: tabular-nums;
  text-align: end;
}

/* A ROW WITH NO FIGURE READS AS A SENTENCE, NOT AS A MISSING NUMBER.
 *
 * It is deliberately NOT the error tone. Retencion and ReteICA are not broken and nothing went
 * wrong; the system simply does not hold the input, and dressing that as a failure would teach a
 * salesperson to dismiss the rows that ARE failures. Same argument as the backorder split. */
.sw-order__breakdown-value--absent {
  max-inline-size: 28ch;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-style: italic;
  font-variant-numeric: normal;
}

.sw-order__breakdown-note {
  margin: var(--space-3) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE O1 GUARD - ADR-0086, question 196. Toned like the backorder notice rather than like an alert,
 * for the same reason: it qualifies a figure, it is not something to act on. */
.sw-order__breakdown-warning {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE ONE PRIMARY CONTROL ON THE VIEW - §8.1 - beneath the figure it commits to. */
.sw-order__place {
  inline-size: 100%;
  margin-block-start: var(--space-4);
}

/* AN ICON-ONLY BUTTON. The accessible name carries the sentence; the glyph carries the meaning.
 * Square at §9 rule 6's 44px, because a row action on a phone is pressed with a thumb. */
.sw-button--icon {
  inline-size: 2.75rem;
  min-block-size: 2.75rem;
  padding: 0;
}

/* -----------------------------------------------------------------------------------
 * THE STEP RAIL.
 *
 * A ROW ABOVE BOTH COLUMNS, which is the fix. The steps were headings inside the layout and a wide
 * container therefore drew 1 and 3 in one column and 2 in the other; the numbering was right and
 * the reading order was 1, 3, 2. Nothing here is decoration for its own sake.
 *
 * EVERY COLOUR IS A LAYER 2 ALIAS. The approved proposal writes raw hex because it is a standalone
 * file; none of it is copied. `--color-surface-selected` is the tinted surface under the current
 * step, `--color-action-primary` the ring and the fill, `--color-border-subtle` the pending
 * outline and the unwalked connector. All three are defined with `light-dark()` in tokens.css, so
 * both themes are correct at the token layer and nothing here has a dark-only branch.
 * ----------------------------------------------------------------------------------- */
.sw-steps {
  display: flex;
  align-items: flex-start;
  gap: 0;
  margin: 0 0 var(--space-6);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-lg);
  background: var(--color-surface-primary);
  list-style: none;
}

.sw-steps__step {
  display: flex;
  align-items: center;
  flex: none;
  min-width: 0;
}

/* THE STEP'S CONTENT, WHICH IS A BUTTON ON A COMPLETED STEP AND A SPAN EVERYWHERE ELSE.
 *
 * ONE RULE FOR BOTH, so a completed step does not shift by a pixel when it becomes a control. The
 * button resets the user agent's own font, padding, border and background rather than inheriting a
 * button appearance the rail never wanted. */
.sw-steps__control {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;

  margin: 0;
  padding: var(--space-1) var(--space-2);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
}

/* Only the button form is interactive, and `button.sw-steps__control` is how the rule says so
 * without a second class that could be put on the wrong element. */
button.sw-steps__control {
  cursor: pointer;
}

button.sw-steps__control:hover {
  background: var(--color-surface-hover);
}

/* 2.5rem, and geometry rather than spacing - the same class of literal `.sw-hero__monogram`'s 4rem
 * is, and with nothing to declare in tokens.css. */
.sw-steps__dot {
  inline-size: 2.5rem;
  block-size: 2.5rem;
  flex: none;

  display: flex;
  align-items: center;
  justify-content: center;

  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-full);
  background: var(--color-surface-secondary);
  color: var(--color-text-secondary);
}

.sw-steps__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.sw-steps__ordinal {
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.sw-steps__label {
  font-family: var(--font-family-display);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-primary);
}

/* WHAT A COMPLETED STEP SETTLED. It is why the picking step needs no customer card: the salesperson
 * can always see who they are ordering for. Quiet, and truncated rather than wrapping, because the
 * rail is one row and a long customer name must not make it two. */
.sw-steps__context {
  overflow: hidden;
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE CONNECTOR. It belongs to the step that follows and is tinted by the state of the step behind
 * it, so a completed step's colour runs into the next one without a third element knowing both. */
.sw-steps__bar {
  flex: 1 1 auto;
  min-inline-size: 1.5rem;
  block-size: 2px;
  margin: 1.25rem var(--space-4) 0;
  border-radius: var(--radius-full);
  background: var(--color-border-subtle);
}

.sw-steps__bar--done {
  background: var(--color-action-primary);
}

/* DONE: filled, and carrying a tick instead of the step's own glyph. */
.sw-steps__step--done .sw-steps__dot {
  border-color: var(--color-action-primary);
  background: var(--color-action-primary);
  color: var(--color-text-on-action);
}

/* CURRENT: a ring on a tinted surface. The ring is a border plus an outline rather than a
 * box-shadow, so it survives forced-colors and does not disappear for a Windows high-contrast
 * reader - §9's rule about state never being carried by colour alone is why the visually hidden
 * "En curso" is in the markup regardless. */
.sw-steps__step--current .sw-steps__dot {
  border-color: var(--color-action-primary);
  background: var(--color-surface-selected);
  color: var(--color-action-primary);
  outline: 3px solid var(--color-surface-selected);
}

.sw-steps__step--current .sw-steps__label {
  color: var(--color-action-primary);
}

/* THE LABELS DROP AND THE DOTS STAY, below the md breakpoint.
 *
 * A CONTAINER QUERY AT 48rem, NOT THE PROPOSAL'S 720px MEDIA QUERY. 720px is not on
 * design-system.md §6's scale (480 / 768 / 1024 / 1280) and §6 wins over the proposal; 48rem is
 * `md`. It is a container query for the reason the columns already are one: this surface answers to
 * the space it is given, so the same partial stays correct in a narrower shell. */
@container sw-order (max-width: 48rem) {
  .sw-steps {
    padding: var(--space-3) var(--space-4);
  }

  .sw-steps__text {
    /* The ordinal, the label and the context all go. The dot, its glyph and the visually hidden
     * state stay, so nothing a screen reader announces changes with the width - and a completed
     * step is still a button, so the navigation survives the drop. */
    display: none;
  }

  .sw-steps__control {
    padding: 0;
  }

  .sw-steps__bar {
    margin-block-start: 1.1875rem;
  }
}

/* THE WAY ON, at the foot of a step. Full width on a narrow container for the same reason
 * `.sw-order__place` is: it is the one thing to press when the step is finished with. */
.sw-order__advance {
  inline-size: 100%;
  margin-block-start: var(--space-4);
}

/* THE CONFIRMING STEP'S HEADER FACTS: who the order is for, and on which list. */
.sw-order__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin: 0 0 var(--space-4);
}

.sw-order__facts dt {
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

.sw-order__facts dd {
  margin: 0;
  font-weight: var(--font-weight-medium);
}

/* -----------------------------------------------------------------------------------
 * THE QUANTITY STEPPER — ADR-0050.
 *
 * THE MARKUP THIS STYLES DOES NOT EXIST UNTIL order-stepper.js RUNS. `.sw-stepper` is a wrapper the
 * script inserts around a number input, and the two buttons are its children. With scripting off
 * none of these rules match anything and the input renders as `.sw-field__control--compact`, which
 * is exactly what it was before this component existed.
 * ----------------------------------------------------------------------------------- */
.sw-stepper {
  display: inline-flex;
  align-items: stretch;
  overflow: hidden;
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  background: var(--color-surface-primary);
}

.sw-stepper__button {
  display: flex;
  align-items: center;
  justify-content: center;

  /* NEVER THE THING THAT SHRINKS. The wrapper is a flex row, so without this the buttons absorb
   * every pixel the field cannot give back - measured at 320px inside the lines table, where one
   * came out 32px wide against section 9 rule 6-s 44px floor. The FIELD may shrink; the targets may
   * not, and OrderQuantityWidthTests fails on the clipping that results if the field shrinks too
   * far. */
  flex: none;

  /* 2.75rem, which is §9 rule 6's 44px target rather than a size chosen to look right. A stepper is
   * pressed repeatedly on a phone, in a pharmacy, by somebody holding something else. */
  inline-size: 2.75rem;
  min-block-size: 2.75rem;

  border: 0;
  background: none;
  color: var(--color-text-secondary);
  cursor: pointer;
}

.sw-stepper__button:hover {
  background: var(--color-surface-hover);
  color: var(--color-action-primary);
}

/* THE INPUT LOSES ITS OWN BOX AND KEEPS THE WRAPPER'S. Two nested borders read as a field inside a
 * field; the focus ring still lands on the input, which is the thing that is actually focused. */
.sw-stepper__value {
  /* SIX REM, AND THE NUMBER CAME FROM A MEASUREMENT RATHER THAN FROM TASTE.
   *
   * It was 4rem, which is 64px of border box and 62px of content box. The product owner reported
   * "la pestaña es muy pequeña para agregar valores como 2000 o 10000. Se corta." — and it did:
   * "10000" measured 99px of content in that 62px box, so 37px of the number was invisible, at
   * every viewport width. Pharmaceutical order quantities are routinely in the thousands.
   *
   * Of those 99px, 32 are this control's own horizontal padding and about 13 were Chromium's native
   * spin buttons, which the rule below removes. OrderQuantityWidthTests measures the result rather
   * than trusting the arithmetic, and it asserts CLIPPING — scrollWidth against clientWidth — not a
   * pixel count, because a width assertion pins whatever number somebody chose and says nothing
   * about whether the text fits once the font or the padding moves.
   *
   * AND THE FIELD'S OWN HORIZONTAL PADDING GOES, which is where the width came back from. A text
   * input pads its content away from the border so the caret is not against it; a stepper value is
   * CENTRED between two buttons, so 32px of that padding is holding nothing. Keeping it at 6rem
   * instead squeezed a stepper button to 32px inside the lines table at 320px — measured — because
   * 44 + 96 + 44 does not fit that cell. */
  inline-size: 4.5rem;
  padding-inline: var(--space-1);
  border-radius: 0;
  border-block: 0;
  border-inline: 1px solid var(--color-border-subtle);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* THE NATIVE SPINNER GOES, AND ONLY HERE. It duplicates the minus and plus either side of it, and
 * it costs about 13px of the space the digits need.
 *
 * THE SCOPE IS THE WHOLE SAFETY OF THIS RULE. `.sw-stepper__value` is applied by order-stepper.js
 * when it builds the wrapper, so it exists only where explicit buttons exist. ADR-0050's guarantee —
 * with scripting off the number input stands alone and is complete, carrying its own spinner — is
 * untouched, because with scripting off this class is never applied. Hiding the spinner on
 * `input[type=number]` generally would have taken that guarantee with it. */
.sw-stepper__value::-webkit-outer-spin-button,
.sw-stepper__value::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}

.sw-stepper__value {
  appearance: textfield;
}

/* The quantity cell: the stepper and the unit it counts, on one line. */
.sw-order__lines .sw-table__value,
.sw-order__lines .sw-table__cell {
  align-items: center;
}

/* THE BASE UNIT beside the quantity. Quiet, because it is the unit of the number rather than a
 * value of its own - "2" and "CAJA" are one fact. */
.sw-order__unit {
  margin-inline-start: var(--space-2);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE QUOTED PRICE ON A PICKER ROW. Tabular figures because a column of prices is scanned down and
 * proportional digits make the decimal points wander. */
.sw-picker__price {
  flex: none;
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A ROW THE SERVER WOULD REFUSE. It is not a button - that is decided in the view, not here - and
 * these rules only stop it LOOKING like one: no pointer, no hover tint, and a quieter name. The
 * chip beside it says why in words, because a dimmed row alone is colour carrying meaning. */
.sw-picker__choice--unavailable {
  cursor: default;
  background: var(--color-surface-secondary);
}

.sw-picker__choice--unavailable .sw-picker__name {
  color: var(--color-text-secondary);
}

/* THE ORDER'S NOTE. A textarea that starts at three rows and grows only if the reader drags it -
 * no script, no auto-resize, because the field is optional and most orders will not use it. */
.sw-order__notes {
  min-block-size: 5rem;
  resize: vertical;
}

/* A STORED NOTE, ON THE PLACED ORDER. white-space: pre-line keeps the line breaks somebody typed
 * without honouring runs of spaces, which is what a paragraph of free text actually wants. */
.sw-order__note {
  white-space: pre-line;
}

/* -----------------------------------------------------------------------------------
 * THE PRODUCT BROWSER — P-ii-f.
 *
 * The picking step is the whole screen now: the lines table moved to the confirming step, so the
 * two-column split this surface used at step 2 is gone with it.
 * ----------------------------------------------------------------------------------- */

/* THE FILTER ROW. Search and category side by side where there is room, stacked where there is
 * not - by CONTAINER query, so the same partial is correct in any width it is given. */
.sw-order__browse {
  display: grid;
  gap: var(--space-4);
  margin-block-end: var(--space-4);
}

@container sw-order (min-width: 34rem) {
  .sw-order__browse {
    /* The search takes the room; the category needs only enough for its longest label. */
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    align-items: end;
  }
}

/* A PRODUCT ROW CARRIES CONTROLS NOW, so it needs more room than a customer row and wraps sooner.
 * The controls stay together as one group when the row wraps - a stepper on one line and its add
 * button on another is two controls that look unrelated. */
.sw-picker--products .sw-picker__choice {
  row-gap: var(--space-3);
}

/* IT STOPS WRAPPING WHERE THERE IS ROOM, AND THIS WAS MEASURED RATHER THAN REASONED.
 *
 * The base row wraps, which is right at 320px and was carried over unchanged. At a TABLET it was
 * still wrapping - 161px tall, three lines - because the row now holds an image, a name, a code
 * chip, a unit, up to two more chips, a price, a stepper and an add button, and their intrinsic
 * widths sum past the container long before the container runs out of usable space.
 *
 * So above 34rem the row is one line and the NAME is what gives: min-width 0 plus the ellipsis
 * below let it truncate instead of pushing everything onto a second row. A product name is the one
 * thing here a reader can still identify from its first forty characters; a price, a stepper and an
 * add button are not. */
/* ON A PHONE THE ROW IS A CARD, AND IT IS 280px TALL. MEASURED, AND LEFT ALONE.
 *
 * At 320px the row wraps to five lines: the thumbnail beside a body that is itself 156px of wrapped
 * name and chips, then the price, then the controls. That is what a product card is on a phone -
 * an image, a name, a code, a unit, stock, what is already ordered, a price and a stepper do not
 * fit on two lines at 320px, and every one of them is information a salesperson quoting from a
 * pharmacy floor asked for.
 *
 * AN ATTEMPT TO COMPRESS IT MEASURED NO CHANGE AND WAS REMOVED. Giving the body the rest of the
 * line beside the thumbnail and pushing the price to the trailing edge produced exactly 280px
 * again; the wrapping is INSIDE the body rather than between the row's own children, so a rule
 * about the row's flex lines cannot reach it. Recorded rather than left as a rule whose stated
 * effect nobody demonstrated.
 *
 * What is verified instead: the row does not overflow, its controls keep §9 rule 6's target, and at
 * TABLET and above it is two lines - 82px, measured. */
@container sw-order (min-width: 34rem) {
  .sw-picker--products .sw-picker__choice {
    flex-wrap: nowrap;
  }

  .sw-picker--products .sw-picker__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The meta line keeps its chips on one line too, for the same reason: a wrapped chip row makes
   * the body two lines and the row two lines with it. */
  .sw-picker--products .sw-picker__meta {
    overflow: hidden;
    white-space: nowrap;
  }
}

.sw-picker__controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;

  /* IT WRAPS AS A UNIT AND NEVER WITHIN ITSELF. */
  white-space: nowrap;
}

/* 44px, which is §9 rule 6's target rather than a size chosen to look right. There are now three
 * controls in one row on a phone - minus, number, plus - plus the add button, all pressed by a
 * thumb in a pharmacy. */
.sw-picker__add {
  min-block-size: 2.75rem;
  flex: none;
}

/* THE ADD BUTTON'S LABEL GOES BELOW `sm`, AND ITS ACCESSIBLE NAME DOES NOT. The glyph and the
 * aria-label carry it - OrderCopy.AddNamed names the product - so nothing a screen reader
 * announces changes with the width. Same rule the step rail's labels follow. */
@container sw-order (max-width: 30rem) {
  .sw-picker__add-label {
    /* NOT display:none on the button itself - only the word goes, so the target keeps its size. */
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .sw-picker__add {
    inline-size: 2.75rem;
    padding: 0;
    justify-content: center;
  }
}

/* THE RUNNING SUMMARY at the picking step: the line count, the total, and the way on.
 *
 * IT IS NOT STICKY, AND THAT IS A DECISION RATHER THAN A GAP. A bar pinned to the bottom of a phone
 * viewport is the obvious answer and it costs a fixed element overlapping the last row, a scroll
 * region that has to know about it, and a second place the add button's 44px target can be covered.
 * The live region is what tells a salesperson their press registered - immediately, without
 * scrolling and without looking - so the figure does not have to chase them. If it turns out it
 * does, that is a measurement on a real phone, not a guess in a stylesheet. */
.sw-order__running {
  margin-block-start: var(--space-5);
}

/* THE CART COUNT ON THE RAIL — P-ii-g, G-3.
 *
 * It rides the step's dot rather than floating, so it is visible from all three steps without a
 * fixed bar and without a new component. Absolutely positioned against the dot, which is why the
 * dot gains `position: relative` below.
 *
 * IT IS aria-hidden AND ANNOUNCED IN WORDS ELSEWHERE. A number in a badge is colour, shape and
 * position; the step's accessible name and its visually hidden state text both carry it. */
.sw-steps__dot {
  position: relative;
}

.sw-steps__count {
  position: absolute;
  inset-block-start: -0.25rem;
  inset-inline-end: -0.25rem;

  display: flex;
  align-items: center;
  justify-content: center;

  /* 1.25rem, and geometry rather than spacing - the same class of literal the dot's own 2.5rem is.
   * min-inline-size rather than a fixed width so a three-digit count grows into a pill instead of
   * spilling out of a circle. */
  min-inline-size: 1.25rem;
  block-size: 1.25rem;
  padding-inline: 0.25rem;

  border-radius: var(--radius-full);
  background: var(--color-action-primary);
  color: var(--color-text-on-action);

  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* A REACHABLE STEP AHEAD ALREADY LOOKS LIKE A CONTROL, AND NEEDED NO NEW RULE.
 * `button.sw-steps__control:hover` above selects on the ELEMENT rather than on the step's state, so
 * it followed the change from backwards-only to both-ways without being touched. A rule keyed on
 * `--done` would have needed editing and a rule keyed on `--pending` would have matched nothing —
 * that modifier does not exist, because pending is what the base rules draw. */

/* THE BACKORDER SPLIT ON A LINE — ADR-0055.
 *
 * A NOTE, NOT AN ALERT. A backorder is a normal commercial state and partial fulfilment is ordinary
 * in this trade; the failure row in the same table uses the alert component because a refusal is
 * something to act on, and making the two look alike is how the one that matters gets dismissed.
 *
 * SECONDARY TEXT AND THE SMALL SIZE, both existing Layer 2 aliases. No new token: the tone is
 * "quieter than the quantity it qualifies", which the type scale already expresses. */
.sw-order__pending {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-block-start: var(--space-1);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE SAME SENTENCE ONCE MORE AT THE CONFIRMING STEP, above the control that commits to it. */
.sw-order__pending-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-block-start: var(--space-3);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* WHAT THE SYSTEM KNOWS ABOUT SUPPLY, PER LINE. Quiet and on its own line under the quantity: it
 * is information beside a figure the reader typed, not a warning about it. */
.sw-order__supply {
  display: block;
  margin-block-start: var(--space-1);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE SENTENCE DATING THOSE FIGURES, once above the lines. */
.sw-order__supply-reading {
  margin-block: var(--space-2) 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* THE WAITING-DRAFT NOTICE ON THE ORDERS LISTING — ADR-0053 as amended.
 *
 * A §8.9 alert with its two ways out beneath it. The alert component carries no action slot and is
 * not given one: it is a message about the page, and a component that can hold arbitrary controls
 * stops being one shape. The wrapper composes them instead - the same relationship .sw-page has to
 * the components it arranges (ADR-0025). */
.sw-draft-notice {
  margin-block-end: var(--space-5);
}

/* TWO REAL FORMS SIDE BY SIDE. Forms cannot nest and each control posts to its own action, so this
 * is two <form> elements laid out as one row rather than one form with two submitters - which
 * ADR-0045 rules out anyway, since submit-guard.js disables the submitter and drops its value. */
.sw-draft-notice__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block-start: var(--space-3);
}

/* THE FORM IS TRANSPORT, NOT LAYOUT - added 2026-09-08, and .sw-dialog__footer-slot is the same
 * reasoning in the same document.
 *
 * THE SAME TWO FORMS NOW FILL TWO DIFFERENT ROWS: .sw-draft-notice__actions on a page, and
 * .sw-dialog__footer inside the waiting dialog. Both are flex containers, and without this the flex
 * ITEM is the <form> rather than the button it carries. On the row above that is merely indirect -
 * one child each, so the gap lands in the same place. In the dialog's footer below `md` it is
 * visible: the footer is a stretching column there, so each form would take a full-width row and
 * the button inside would keep its own width. Every other dialog puts its buttons directly in the
 * footer; display: contents is how these two join them without a second copy of the markup.
 *
 * IT CHANGES NO SEMANTICS. display: contents removes the element's own box, not the element - the
 * form still submits, still carries its data-sw-confirm attributes, and is still what
 * requestSubmit() is called on. */
.sw-draft-notice__form {
  display: contents;
}

/* THE FACT, STATED AS PROSE RATHER THAN AS A NOTICE - added 2026-09-08, design-system.md §8.12.
 *
 * NO BOX, NO BORDER, NO ICON: inside a dialog the frame is already the notice, and _Alert's own
 * container drew a second one. What is left is a paragraph, and the only thing it needs is not to
 * inherit a margin from the user agent - the dialog's body is a flex column with its own gap, and a
 * default <p> margin would add to it unevenly. */
.sw-draft-notice__fact {
  margin: 0;
  color: var(--color-text-primary);
}

/* THE ORDER'S LOG — ADR-0056. An ordered list, most recent first.
 *
 * AN <ol> AND NOT A TABLE, because the entries have different shapes: a dispatch carries an invoice
 * number, a cancellation carries a reason and a note, a placing carries neither. A table would draw
 * three empty cells on most rows to keep a grid nobody is scanning across. */
.sw-timeline {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sw-timeline__entry {
  padding-inline-start: var(--space-4);

  /* THE RULE DOWN THE LEADING EDGE is the only ornament, and it is what makes the entries read as
   * one sequence rather than as stacked paragraphs. Same inset-mark idea as the sidebar's current
   * item, which is where the pattern already exists. */
  border-inline-start: 2px solid var(--color-border-subtle);
}

.sw-timeline__what {
  margin: 0;
  font-weight: var(--font-weight-semibold);
}

.sw-timeline__who,
.sw-timeline__why,
.sw-timeline__note {
  margin: var(--space-1) 0 0;
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
}

/* WHAT SOMEBODY TYPED, so it wraps rather than stretching the panel. The same treatment the order's
 * own note gets. */
.sw-timeline__note {
  overflow-wrap: anywhere;
}

/* THE TWO LIFECYCLE ACTIONS, side by side where there is room. Two real forms - they post to
 * different actions and forms cannot nest - laid out as one row. */
.sw-order__lifecycle {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}

.sw-order__lifecycle-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);

  /* A COLUMN THAT DOES NOT STRETCH TO THE PANEL. --layout-form-max-width is ADR-0025's token for
   * exactly this: a full-bleed text input is the main "unfinished" tell. */
  flex: 1 1 var(--layout-form-max-width);
  max-inline-size: var(--layout-form-max-width);
}

/* THE ORDER'S TOTAL, OUTSIDE THE LINES TABLE — P-ii-l.
 *
 * It is not a cell, and the reason is announcement rather than layout: a figure inside the table is
 * read as part of a row, so a screen reader would announce the order's total as though it were a
 * line item. The same reasoning §8.13 gives for keeping the summary on its own surface. */
.sw-order__paid {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-4);
  border-block-start: 1px solid var(--color-border-subtle);
}

.sw-order__paid-label {
  color: var(--color-text-secondary);
  font-weight: var(--font-weight-semibold);
}

/* THE FIGURE A READER IS LOOKING FOR, at the size §8.13 already settled for a total - 2xl in the
 * display face, on the type scale, with tabular figures so it does not shift when it changes. */
.sw-order__paid-figure {
  color: var(--color-action-primary);
  font-family: var(--font-family-display);
  font-size: var(--font-size-2xl);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------------------
 * §8.14 THE SEGMENTED FILTER — one row of mutually exclusive choices, each a LINK.
 *
 * LINKS RATHER THAN BUTTONS, because on every surface that uses this the filter lives in the query
 * string (ADR-0033), so choosing a segment is a navigation. SegmentedFilterModel carries the whole
 * argument, including why the approved mockup's aria-pressed buttons were not followed.
 * --------------------------------------------------------------------------------------- */
.sw-segmented {
  display: inline-flex;
  gap: var(--space-1);
  padding: var(--space-1);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-full);
  background: var(--color-surface-secondary);

  /* IT SCROLLS RATHER THAN WRAPPING. Four segments fit at 320px only by shrinking the type below
   * §6's floor; scrolling keeps every one reachable at full size, which is what §9 asks.
   *
   * AND UNTIL 2026-09-07 IT NEVER SCROLLED ONCE. Measured on /commercial/orders at 320: the nav's
   * box was 721px inside a 320px viewport with `scrollWidth` 719 — a scroll container WIDER than
   * its own content, which is the shape of a box that was never constrained. `max-inline-size: 100%`
   * resolves against a parent that was itself sized by this element's min-content, so 100% was 721.
   * The two declarations below were correct and inert; what was missing was upstream. */
  max-inline-size: 100%;
  overflow-x: auto;

  /* THE CONTAINING BLOCK FOR WHAT IS INSIDE IT, and this is the third of question 89's three causes
   * — the one that survived fixing the other two.
   *
   * Each segment carries a `.sw-visually-hidden` span for its count, and that utility is
   * `position: absolute`. With nothing positioned between it and the page, its containing block was
   * the initial one, so it was NOT CLIPPED BY THIS SCROLL CONTAINER: five 1px boxes sat at their
   * static positions out to 653px and stretched the document by themselves. Measured — with the
   * shell capped and the toolbar shrinking, the page still scrolled to exactly 653, and the last
   * span's right edge was exactly 653.
   *
   * ON THE NAV RATHER THAN ON THE OPTION, so it contains any absolutely positioned descendant at
   * any depth rather than only the spans that exist today.
   *
   * AND IT DOES NOT HIDE THEM FROM A SCREEN READER, which is the thing to check before believing
   * this fix: measured after the change, the span is still `display: block`, `visibility: visible`,
   * 1x1, reading "1 pedido". Clipped, not removed — the distinction §8.5's header rules already
   * turn on. */
  position: relative;

  /* The bar is hidden and the row still scrolls by touch, wheel and keyboard. */
  scrollbar-width: none;
}

.sw-segmented::-webkit-scrollbar {
  display: none;
}

/* A SWITCH DIRECTLY INSIDE A DIALOG BODY KEEPS ITS OWN HEIGHT — open question 167.
 *
 * MEASURED BROKEN IN PRODUCTION, 2026-09-14: the Bonificaciones kind switch rendered as a 464 x 10 px strip
 * with both labels clipped out of sight. `.sw-dialog__body` is a flex COLUMN that shrinks its children when
 * the dialog's content is taller than the dialog, and `overflow-x: auto` above makes this element a scroll
 * container, whose automatic minimum size in a flex container is zero. So the column shrank it to its
 * padding and border.
 *
 * SCOPED TO THE DIALOG BODY RATHER THAN WRITTEN ON `.sw-segmented`, and that is the decision. On the orders
 * and receivables listings this component sits in a horizontal toolbar that relies on it SHRINKING along
 * the row so its own overflow can scroll at 320px; `flex-shrink: 0` on the component would stop that. In a
 * column, `flex-shrink` governs the block size only, so here it stops the collapse and leaves the
 * horizontal scrolling untouched. Nor is it written on every child of `.sw-dialog__body`: that would change
 * how every dialog in the product distributes height, to fix one component in one of them.
 *
 * Guarded by BonusCampaignDialogGeometryTests, which opens the dialog and measures the box. */
.sw-dialog__body > .sw-segmented {
  flex-shrink: 0;
}

.sw-segmented__option {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: var(--space-2);

  /* 44px OF HEIGHT, §9 rule 6, because this is pressed on a phone. The padding is what produces it
   * rather than a fixed size, so a longer label grows the pill instead of clipping. */
  min-block-size: 2.75rem;
  padding-inline: var(--space-4);

  border-radius: var(--radius-full);
  color: var(--color-text-secondary);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  text-decoration: none;
  white-space: nowrap;
}

.sw-segmented__option:hover {
  color: var(--color-text-primary);
}

/* THE ONE IN FORCE. Every carrier hangs off aria-current, so the announcement and the appearance
 * cannot disagree — one fact, one selector. THREE CARRIERS, not one: the surface, the text colour
 * and the weight. §9 rule 3, colour is never the only signal. */
.sw-segmented__option[aria-current="page"] {
  background: var(--color-surface-primary);
  color: var(--color-tone-brand-fg);
  font-weight: var(--font-weight-semibold);
  box-shadow: var(--shadow-xs);
}

/* THE COUNT. Tabular figures so the row does not shift as the numbers change, and the same pill
 * shape at a smaller scale. */
.sw-segmented__count {
  padding-inline: var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-surface-secondary);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}

.sw-segmented__option[aria-current="page"] .sw-segmented__count {
  background: var(--color-navigation-item-active);
  color: var(--color-tone-brand-fg);
}

/* THE FILTER ROW — search and the segmented control on ONE line, inside the results panel's head
 * rather than in a panel of their own.
 *
 * IT WAS TWO FULL .sw-field BLOCKS PLUS A BUTTON IN A SEPARATE PANEL, which is the height the
 * product owner was complaining about: label above control, twice, above the table.
 *
 * IT SHIPPED WITHOUT flex-direction AND WAS THEREFORE A COLUMN — the base .sw-toolbar sets
 * `flex-direction: column`, and a modifier that redeclares `display: flex` inherits the direction
 * it did not mention. Measured at 320, 768 and 1280: computed direction `column` at all three, the
 * toolbar 342-346px tall, and the search field exactly 192px because `flex: 1 1 12rem` below became
 * a BLOCK basis. `align-items: flex-end` then pinned every control to the right-hand edge, since in
 * a column the cross axis is the inline one. One missing declaration, four wrong outcomes.
 *
 * THE OTHER THREE DECLARATIONS WENT WITH IT, because re-reading the block showed them to be no-ops
 * duplicating the base: `display: flex`, `gap: var(--space-3)` and a `padding-block-end` equal to
 * the `padding` already set. A modifier that restates its base is a modifier nobody can read for
 * what it actually changes - which is how the one declaration that mattered went missing. */
.sw-toolbar--inline {
  flex-direction: row;
  flex-wrap: wrap;

  /* IT SITS IN THE PANEL'S HEAD RATHER THAN ON A SURFACE OF ITS OWN. The base .sw-toolbar paints
   * --color-surface-secondary, which §3 annotates "table headers, filter bars" and §8.3 assumes -
   * and §8.3 was written for a filter bar FLOATING ON A PAGE. Inside a panel that already has its
   * own surface it makes a grey slab in a white box: two nested fills, which is what reads as a box
   * in a box. A bottom border does the separating instead, which is what a panel head is.
   *
   * The inline padding goes with the fill: the panel's own inset already holds the row off the
   * edge, and keeping both indented the controls twice. */
  padding-inline: 0;
  padding-block-start: 0;
  border-block-end: 1px solid var(--color-border-subtle);
  border-radius: 0;
  background: none;

  /* On a row this is the cross axis, so the button and the segmented control sit level with the
   * bottom of the field's control rather than with the top of its label. */
  align-items: flex-end;
}

/* ---- The toolbar as a panel's head, without the row layout ----
 *
 * SAME SURFACE ANSWER AS --inline, WITHOUT ITS FLEX DIRECTION, AND THE SPLIT WAS FORCED BY A
 * MEASUREMENT. --inline bundles two decisions: stop painting a surface, and lay the controls out in
 * a row. That is right where the toolbar IS the filter form, which is how the orders listing uses
 * it - the modifier's own child rules (.sw-toolbar--inline .sw-field) are written for that shape.
 *
 * THE USERS LISTING IS SHAPED DIFFERENTLY: a .sw-toolbar wrapping a separate .sw-filters form that
 * already owns its own layout at every breakpoint. Applying --inline there took the filter row from
 * 293px to 617px at 320px wide - measured, both themes - because the row direction and flex-end
 * alignment fought a child that was already laying itself out.
 *
 * So this carries only the part X-3 is about: the nested fill. A panel that already has a surface
 * does not want a second one inside it, and a bottom border is what a panel head is. */
.sw-toolbar--flush {
  padding-inline: 0;
  padding-block-start: 0;
  border-block-end: 1px solid var(--color-border-subtle);
  border-radius: 0;
  background: none;
}

/* NO TOOLBAR ITEM MAY REFUSE TO SHRINK, and this is the second of open question 89's three causes.
 *
 * A flex item's `min-width` is `auto`, which resolves to its CONTENT-BASED MINIMUM — so an item
 * wider than the line does not shrink, it overflows, and on a grid page with an uncapped track it
 * stretches the page instead. The status filter's container is exactly that: a plain wrapper around
 * a `.sw-segmented` whose `overflow-x: auto` was written to make it scroll and could never fire,
 * because nothing in the chain would let the box be narrower than the pills inside it.
 *
 * MEASURED, 2026-09-07, /commercial/orders at 320: with the shell capped the document still scrolled
 * to 761px; this rule takes it to 653.
 *
 * IT IS THE GENERAL STATEMENT RATHER THAN ANOTHER PER-CHILD RULE. `.sw-field` below already carries
 * its own `min-inline-size: 0` — one child, fixed once somebody noticed it — and the next control
 * added to this row would have arrived with the same defect and no rule. The declaration on
 * `.sw-field` is now redundant and is KEPT: it states the intent where the flex basis is set, and
 * removing it would be a change to a rule this pass did not measure.
 *
 * SCOPED TO --inline DELIBERATELY. `.sw-toolbar--flush` wraps a `.sw-filters` form that already lays
 * itself out at every breakpoint, and the comment above records that toolbar modifiers fighting that
 * child took the users listing from 293px to 617px. This pass measured the inline shape; it did not
 * measure that one. */
.sw-toolbar--inline > * {
  min-inline-size: 0;
}

/* The search grows and the rest keeps its size, so on a narrow screen the field is what wraps. */
.sw-toolbar--inline .sw-field {
  flex: 1 1 12rem;
  min-inline-size: 0;
  margin-block-end: 0;
}

/* THE WAY BACK, LEADING THE PAGE — P-ii-l. The order detail's only route back was a link at the
 * foot that went to "Nuevo pedido", which is a different place from where the reader came from. */
.sw-page-back {
  margin-block-end: var(--space-3);
}

/* A TEXT LINK IN THE BRAND COLOUR, with the underline the base stylesheet gives every anchor. */
.sw-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-tone-brand-fg);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
}

/* THE GLYPH POINTS BACK. The sprite has one chevron and it points right, so it is turned rather
 * than a second symbol being added - the sprite is already past the ~30 it sets for itself. */
.sw-page-back .sw-icon {
  transform: rotate(180deg);
}

/* §9 rule 6: 44x44 below lg. MEASURED AT 136x21 on the order detail at 390 and 430 on 2026-09-13,
 * which is less than half the rule, on the control a salesperson uses to get back to their list.
 *
 * IT IS SCOPED TO .sw-page-back RATHER THAN APPLIED TO .sw-link, and that is the whole decision.
 * .sw-link is also the class for links INSIDE prose, where a 44px minimum would blow a hole in the
 * line box it sits in. A standalone navigation control is what the touch rule is about; a link in a
 * sentence is reached by tapping words, and the rule has never claimed otherwise.
 *
 * THE HEIGHT GROWS AND THE TEXT DOES NOT, the same way .sw-button does it above: a bigger target
 * rather than a bigger-looking link. align-items keeps the glyph and the words centred in the taller
 * box instead of leaving them at the top of it.
 *
 * IT WAS INVISIBLE UNTIL 2026-09-13 because the order detail had never been in the responsive
 * probe's route list - not because the rule was unclear. docs/measurements/responsive-2026-09-13-demo.md. */
@media (max-width: 1023px) {
  .sw-page-back .sw-link {
    min-height: 44px;
    align-items: center;
  }
}

/* THE STATUS BADGE BESIDE THE TITLE. The status is part of what the order IS; on a line of its own
 * it read as a caption. */
.sw-page-header__badge {
  margin-block-start: var(--space-2);
}

/* A PANEL WHOSE CHILD OWNS THE PADDING — the fact strip draws its own cells edge to edge, and the
 * panel's inset would leave a gutter the dividers stop short of. */
.sw-panel--flush {
  padding: 0;
  overflow: hidden;
}

/* THE FACT STRIP. A <dl>, so a screen reader pairs each term with its value instead of reading
 * loose strings — the same reasoning §8.13's summary carries.
 *
 * THE 1px GAP OVER A BORDER COLOUR is what draws the dividers: each cell paints its own background,
 * and the gap between them shows the container through. One rule instead of per-cell borders that
 * double up at the joins. */
.sw-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 1px;
  margin: 0;
  background: var(--color-border-subtle);
}

.sw-fact {
  padding: var(--space-4) var(--space-5);
  background: var(--color-surface-primary);
}

.sw-fact dt {
  margin-block-end: var(--space-1);
  color: var(--color-text-secondary);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.sw-fact dd {
  margin: 0;
  font-weight: var(--font-weight-medium);
}

/* THE TOTAL IS THE FIGURE A READER LOOKS FOR, so it carries the weight §8.13 already settled for
 * one — the display face, on the type scale, with tabular figures. */
.sw-fact--figure dd {
  color: var(--color-action-primary);
  font-family: var(--font-family-display);
  font-size: var(--font-size-xl);
  font-weight: var(--font-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* A LIFECYCLE ACTION'S FORM — the same partial inside the dialog and on its own page. */
.sw-order-action {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.sw-order-action__body {
  margin: 0;
  color: var(--color-text-secondary);
}

/* THE CONTROLS AT THE FOOT, dismissal first in SOURCE order so a keyboard reader reaches the
 * harmless one first — the same arrangement _ConfirmDialog records, and for the same reason. */
.sw-order-action__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: flex-end;
}

/* A FORM STANDING ON ITS OWN PAGE keeps the width §8's form column already settles, so a text input
 * does not stretch the width of a desktop. ADR-0025's token. */
.sw-panel--form {
  max-inline-size: var(--layout-form-max-width);
}

/* THE TIMELINE'S ENTRIES READ DIFFERENTLY BY KIND — P-ii-l.
 *
 * DERIVED FROM THE EVENT KIND, not from the copy, so the three cannot drift apart. The mark is a
 * colour AND a weight: §9 rule 3, colour is never the only carrier. */
.sw-timeline__entry--placed {
  border-inline-start-color: var(--color-action-primary);
}

.sw-timeline__entry--invoiced {
  border-inline-start-color: var(--color-status-success-fg);
}

/* A CANCELLED ENTRY IS THE ONE A READER SCANS FOR, so it is the one that differs most: a muted rule
 * and a lighter heading say "this is where it stopped" without shouting. */
.sw-timeline__entry--cancelled {
  border-inline-start-color: var(--color-text-secondary);
}

.sw-timeline__entry--cancelled .sw-timeline__what {
  color: var(--color-text-secondary);
}

/* ---------------------------------------------------------------------------
   The picking queue — §8.14

   A WORK LIST, NOT A TABLE. Every row carries a claim, two controls and a wait, and a table would
   put the controls in a cell whose width is decided by the widest label in the column. The mockup
   at docs/brand/bodega.html draws it as rows; this is that shape in the real token layer.

   CONTAINER QUERIES, NOT VIEWPORT MEDIA QUERIES. What changes is the ROW's own arrangement, and a
   component that responds to its own box survives being moved into a narrower panel — which this
   one will be, the day the queue appears beside something else. The repository already uses this
   for sw-detail and sw-order; the reasoning is written there and is not repeated.
   --------------------------------------------------------------------------- */
.sw-queue {
  container-type: inline-size;
  container-name: sw-queue;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* THE BASE IS THE PHONE, and that is the design rather than a fallback. One card per order, the
 * parts stacked in reading order — what it is, who has it, what you can do — and the controls last
 * because a thumb reaches the bottom of a card more easily than its middle. */
.sw-queue__row {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--color-surface-primary);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

/* SOMEBODY ELSE'S CLAIM IS MARKED ON THE ROW AS WELL AS IN THE BADGE, and the badge is what carries
 * the meaning: §8.4's rule is that colour reinforces and never signals. This border is the
 * reinforcement — a reader scanning twenty rows finds the held ones by shape, then reads the name. */
.sw-queue__row--held {
  border-inline-start: 3px solid var(--color-status-warning-solid);
}

.sw-queue__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* SO A LONG CUSTOMER NAME SHRINKS INSTEAD OF PUSHING THE ACTIONS OFF. min-width:auto is the flex
   * default and it refuses to shrink below the content's intrinsic width, which is what produces a
   * horizontal scrollbar at 320px. */
  min-inline-size: 0;
}

.sw-queue__title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--font-weight-semibold);
}

.sw-queue__meta,
.sw-queue__side {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
  /* A customer name with no spaces cannot be broken by wrapping alone. */
  overflow-wrap: anywhere;
}

/* A COLUMN ON THE PHONE, WHICH IS WHAT "full-width and thumb-reachable" MEANS.
 *
 * IT WAS A WRAPPING ROW AND THAT WAS THE DEFECT. Two controls at a 8rem minimum both fit on one
 * line inside a 320px card, so they sat side by side at their natural width with the rest of the
 * row empty - the brief asked for full-width and the base layer quietly did not deliver it.
 *
 * EACH CHILD IS A <form> OR AN <a>, so the width has to be set on the child AND on the button
 * inside it; a form is a block that would otherwise stretch while its button stayed inline-flex. */
.sw-queue__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.sw-queue__actions > * {
  inline-size: 100%;
}

/* 44px, FROM THE TOKEN RATHER THAN A LITERAL. --layout-touch-target-min is what §9 rule 6
 * specifies; .sw-button's own base is 40px, which is the §8.1 control height and is smaller. The
 * height is raised at EVERY width rather than below a breakpoint: this is a warehouse screen used
 * on a phone in one hand, and a 44px control on a desktop is not a cost. */
.sw-queue__actions .sw-button {
  --sw-button-height: var(--layout-touch-target-min);

  display: flex;
  inline-size: 100%;
}

/* ---- The row layout arrives when the ROW can hold it ----------------------
 *
 * 34rem is the same threshold sw-detail uses for its label/value grid and it is the same question:
 * is there room for a second column that is still readable. Below it the card stacks; at it the
 * identity and the wait sit side by side and the actions drop under both. */
@container sw-queue (min-width: 34rem) {
  .sw-queue__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "main side"
      "actions actions";
    align-items: start;
    column-gap: var(--space-4);
  }

  .sw-queue__main { grid-area: main; }

  .sw-queue__side {
    grid-area: side;
    text-align: end;
    /* SO THE WAIT NEVER CRUSHES THE CUSTOMER NAME. The side column is auto-sized, and without this
     * a long holder name would take width the identity needs. */
    max-inline-size: 14rem;
  }

  /* SIDE BY SIDE ONCE THE CARD CAN HOLD BOTH, and each stops growing so two buttons do not
   * stretch across a wide row. The column above is for the width where they cannot share a line
   * without one of them shrinking under its label. */
  .sw-queue__actions {
    grid-area: actions;
    flex-direction: row;
    justify-content: flex-end;
  }

  .sw-queue__actions > *,
  .sw-queue__actions .sw-button {
    inline-size: auto;
  }
}

/* ---- Three columns, which is the mockup's desktop row ---------------------
 *
 * 52rem, because that is where the actions fit BESIDE the wait rather than under it without either
 * column dropping below its content. Measured against the widest label pair this screen renders —
 * "Tomar de todas formas" and "Ver". */
@container sw-queue (min-width: 52rem) {
  .sw-queue__row {
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas: "main side actions";
    align-items: center;
  }

  .sw-queue__actions {
    justify-content: flex-end;
  }
}

/* THE ONE BADGE IN THE PRODUCT THAT CARRIES A PERSON'S NAME, and it is why this override exists.
 *
 * .sw-badge sets white-space: nowrap, correctly: every other badge in the catalogue is a short
 * status word from §8.4's closed vocabulary - Activo, Entregado, Retirado - and letting those wrap
 * would look broken. The holder badge interpolates a display name, which is unbounded, and MEASURED
 * at 320 it pushed the document to 333 against a 320 viewport. WCAG 1.4.10 is the floor, so that is
 * an accessibility defect rather than a cosmetic one.
 *
 * WRAPPING RATHER THAN TRUNCATING, because the name is the entire point of the badge: a reader
 * about to take an order away from somebody needs to know who. An ellipsis on the one word that
 * matters is worse than a pill two lines tall. */
.sw-queue__title .sw-badge {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* =====================================================================================
 * THE PICKING SHEET — screen 2 of docs/brand/bodega.html.
 *
 * THE MOST PHONE-SHAPED SCREEN IN THE PRODUCT, and the base layout is written for it rather
 * than inherited from a desktop that gets narrowed. A person holds a box in one hand: the
 * quantity field, the scan field and the close button are what a thumb has to reach, so all
 * three are full-width at the base and the close control is the last thing in the document.
 *
 * IT LAYS OUT BY CONTAINER WIDTH, NOT VIEWPORT — the same construction the customer detail
 * uses, so one partial renders correctly whatever it is placed inside.
 * ===================================================================================== */

.sw-page--sheet {
  container-type: inline-size;
  container-name: sw-sheet;
}

/* ---- The meter -------------------------------------------------------------------- */

/* A NATIVE <progress>, WHICH IS WHAT MAKES A DATA-DRIVEN BAR POSSIBLE AT ALL HERE.
 *
 * The mockup draws a bar whose width comes from the data, and a width is a style attribute —
 * refused on every request by ADR-0006's CSP, silently. Open question 67 is what that costs:
 * TableColumn.Width emitted exactly such an attribute and did nothing for as long as it
 * existed, with green markup tests over it the whole time.
 *
 * <progress> carries the value as an ATTRIBUTE, which no policy blocks, and brings the role
 * and the announcement with it. The two pseudo-elements below are the price: there is no
 * single standard selector for the filled part, so both vendor forms are written and neither
 * can be folded into the other — a selector list containing one unknown pseudo-element is
 * dropped entirely by the parser. */
.sw-sheet__progress {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.sw-sheet__progress-label {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

.sw-meter {
  inline-size: 100%;
  block-size: 0.5rem;
  border: 0;
  border-radius: var(--radius-full);
  background-color: var(--color-surface-secondary);

  /* Firefox styles the TRACK through the element itself; WebKit needs the pseudo-element
   * below. Both are set so the two engines agree. */
  appearance: none;
  overflow: hidden;
}

.sw-meter::-webkit-progress-bar {
  background-color: var(--color-surface-secondary);
}

.sw-meter::-webkit-progress-value {
  background-color: var(--color-action-primary);
}

.sw-meter::-moz-progress-bar {
  background-color: var(--color-action-primary);
}

/* ---- The scan panel ---------------------------------------------------------------- */

/* THE SCAN FIELD AND ITS BUTTON SHARE A ROW AND THE HINT SITS UNDER BOTH, so the field is as
 * wide as it can be at 320 — it is what a scanner types into and what somebody types a code
 * into by hand. */
.sw-sheet__scan {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-2) var(--space-3);
  align-items: end;
}

.sw-sheet__scan > .sw-field {
  min-width: 0;
}

.sw-sheet__hint {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

/* FULL WIDTH AND ON ITS OWN ROW. It is the control for somebody with no scanner, and it is
 * pressed once per order rather than once per box — so it gets a whole row rather than
 * competing with the scan button for the corner a thumb reaches. */
.sw-sheet__fill {
  grid-column: 1 / -1;
  justify-content: center;
}

/* ---- The lines --------------------------------------------------------------------- */

.sw-sheet__lines {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* ONE CARD PER LINE AT THE BASE, stacked in the order the picker works: what to find, how many
 * were taken, off which lot. */
.sw-sheet__line {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-block-end: var(--space-4);
  border-block-end: 1px solid var(--color-border-subtle);
}

.sw-sheet__line:last-child {
  padding-block-end: 0;
  border-block-end: 0;
}

/* A LINE WITH NO BALANCE IS DIMMED AND STILL DRAWN. It is part of the order and its absence
 * would make the sheet disagree with the order detail; what it must not do is invite a
 * quantity, and the port refuses one anyway. */
.sw-sheet__line--settled {
  opacity: 0.6;
}

.sw-sheet__line-main {
  min-width: 0;
}

.sw-sheet__line-title {
  margin: 0;
  font-weight: var(--font-weight-medium);
  color: var(--color-text-primary);
}

.sw-sheet__line-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-1) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

.sw-sheet__line-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.sw-sheet__cap {
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

/* ---- The footer -------------------------------------------------------------------- */

/* THE SENTENCE SITS ABOVE THE CONTROL THAT COMMITS TO IT, which is the same order the order
 * summary uses: read what will happen, then press the thing that does it. */
.sw-sheet__foot {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.sw-sheet__what {
  margin: 0;
  color: var(--color-text-primary);
}

/* THE CLOSE BUTTON IS FULL-WIDTH AND LAST AT THE BASE, WHICH IS WHERE A THUMB REACHES.
 *
 * PLAIN column, NOT column-reverse. The release control is first in source and the close is
 * last, so an unreversed stack already puts the primary act at the bottom AND leaves the tab
 * order meeting the quiet control first. column-reverse was written here and would have done
 * the opposite of what this comment claimed - the visual order and the reading order both
 * flipped, so the destructive-adjacent control would have sat under the thumb. */
.sw-sheet__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.sw-sheet__actions > .sw-button {
  justify-content: center;
}

@container sw-sheet (min-width: 34rem) {
  /* THE LINE BECOMES A ROW: what to find takes the space, the two fields sit at the end. */
  .sw-sheet__line {
    display: grid;
    grid-template-columns: 1fr 8rem 10rem;
    gap: var(--space-3);
    align-items: end;
  }

  .sw-sheet__actions {
    flex-direction: row;
    justify-content: flex-end;
  }

  .sw-sheet__fill {
    grid-column: auto;
  }

  .sw-sheet__scan {
    grid-template-columns: 1fr auto auto;
  }
}

/* THE PRODUCT CODE, WHICH IS DATA RATHER THAN COPY. Monospace so a picker comparing it against
 * what is printed on a box is comparing glyphs of the same width — a 0 and an O, an I and a 1
 * are the misreads that matter here and a proportional face hides both. */
.sw-sheet__code {
  /* A LITERAL STACK RATHER THAN A TOKEN, AND THE ARCHITECTURE TEST IS WHY.
   *
   * This was written as var(--font-family-mono, ui-monospace, monospace) and there is no such
   * token. The fallback would have worked in a browser, which is exactly the problem: the
   * declaration would have looked token-driven for ever while resolving to its fallback, and
   * DesignTokenLayerTests refuses an undeclared custom property for that reason.
   *
   * ADDING THE TOKEN IS A TOKEN DECISION AND THIS IS NOT THE PASS FOR IT. There is one monospace
   * consumer in the product; §6 sizes a token by having more than one. When a second arrives, the
   * two get a token and this rule loses its stack. */
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", "Liberation Mono", monospace;
  font-size: var(--font-size-sm);
  color: var(--color-text-primary);
}

/* ---------------------------------------------------------------------------------------------
 * THE AGEING STRIP — the 30/60/90 ladder, compact. §8.5's figures, not §8.8's cards.
 *
 * IT REPLACED A PANEL OF FIVE THREE-LINE TILES ON 2026-09-05. Those tiles carried an amount and a
 * document count each - ten of the fourteen figures the cartera used to open with, which is the
 * density the client read as too full. The bands and the numbers are unchanged; what changed is
 * that they read as a scale rather than as five competing statistics.
 *
 * WHY NOT .sw-stat-row: that grid is for a handful of independent KPIs and gives each tile a card
 * with 8rem of minimum width. A ladder is one sequence read left to right, and five carded tiles
 * wrapping 3 + 2 on a phone break the sequence into two rows that no longer read in order.
 */
.sw-ageing {
  /* auto-fit WITH A SMALL MINIMUM, so five bands sit on one line on a desktop and fold to two or
   * three per row on a phone - in order, because the source order is the ladder order. No
   * breakpoint and no count in the stylesheet that has to agree with how many bands the projection
   * emitted; ReceivableSummary.Ladder is always five, and the rule does not depend on that. */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: var(--space-2);
  margin-block-start: var(--space-3);
}

/* THE RUNG. A left border rather than a full card: it groups the three lines without drawing five
 * boxes, which is what made the old tiles compete with the summary above them. */
.sw-ageing__band {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding-inline-start: var(--space-3);
  border-inline-start: 2px solid var(--color-border-subtle);
}

.sw-ageing__label {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
  color: var(--color-text-secondary);
}

/* THE ONE FIGURE BEING COMPARED ACROSS BANDS, so it gets the tabular figures §8.5 asks for wherever
 * magnitudes line up in a column - here the column is the strip. */
.sw-ageing__amount {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-primary);
}

/* THE COUNT, DEMOTED AND STILL VISIBLE. It is not in a title attribute: a tooltip is unreachable by
 * keyboard, announced inconsistently, and absent on the phone this restructure is for. */
.sw-ageing__count {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}

/* ---------------------------------------------------------------------------------------------
 * THE DISTRIBUTION BAR - the same five bands as a proportion. ADR-0072, design-system.md 8.16.
 *
 * NO CHARTING LIBRARY AND NO SCRIPT. Section 11 item 4 is still open for the dashboard's
 * multi-series case; this is the product's first data visualisation and it needed neither. The CSP
 * admits no external script (ADR-0006) and the design system's own CSP section offers two roads for
 * a data-driven value: a data-* attribute read by an external script, or a closed scale of classes.
 * The second is what ADR-0038 chose for column spans and it is what this uses, because a bar drawn
 * by the stylesheet is drawn on the first paint rather than after a script that may not arrive.
 *
 * WHY IT IS NOT A style ATTRIBUTE: open question 67 records TableColumn.Width emitting exactly that,
 * being refused silently on every render, and every markup test over it staying green.
 */
.sw-ageing__bar {
  display: flex;

  /* THE TRACK CLIPS ITS OWN CORNERS, so the first and last segments are rounded without either of
   * them knowing which end it is on - a rule per position would be wrong the moment a band is
   * empty and takes no width. */
  overflow: hidden;
  block-size: var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--color-surface-secondary);
  margin-block-start: var(--space-3);
}

/* THE SEGMENT. Its width is the two custom properties below, added together - so 0 to 100 whole
 * percent is twenty-one rules rather than a hundred and one, and a class that does not exist
 * contributes nothing rather than something wrong.
 *
 * NO GAP BETWEEN SEGMENTS: the widths sum to exactly 100, and a gap would push the last band past
 * the end of the track where it is clipped. They are separated by tone and by the hairline below. */
.sw-ageing__fill {
  inline-size: calc((var(--sw-share-tens, 0) * 10%) + (var(--sw-share-units, 0) * 1%));

  /* A HAIRLINE OF THE TRACK BETWEEN NEIGHBOURS, drawn as a border rather than a gap so it comes out
   * of the segment's own width instead of adding to the row. box-sizing is border-box globally. */
  border-inline-end: 1px solid var(--color-surface-primary);
}

.sw-ageing__fill:last-child {
  border-inline-end: 0;
}

/* THE FIVE TONES, AND THEY ARE A SCALE RATHER THAN FIVE STATUSES. The ladder runs from money not yet
 * due to money three months late, so the bar reads as one gradient of severity - which is the whole
 * of what a distribution is for. They are the status foreground tokens because those are the five
 * this product has calibrated for contrast, and the badge in each row already uses the same mapping
 * (ReceivableFormat.AgeBadge): por vencer is info, the first band pending, the middle two warning,
 * over ninety error. A sixth colour invented here would be a sixth thing to calibrate. */
.sw-ageing__fill--notdue {
  background: var(--color-status-info-fg);
}

.sw-ageing__fill--days1to30 {
  background: var(--color-status-pending-fg);
}

.sw-ageing__fill--days31to60 {
  background: var(--color-status-warning-fg);
}

.sw-ageing__fill--days61to90 {
  background: var(--color-status-warning-fg);
}

.sw-ageing__fill--over90 {
  background: var(--color-status-error-fg);
}

/* THE TWO DIGITS OF THE SHARE. AgeingRung.ShareClass emits one of each, always both. */
.sw-ageing__fill--t0 {
  --sw-share-tens: 0;
}

.sw-ageing__fill--t1 {
  --sw-share-tens: 1;
}

.sw-ageing__fill--t2 {
  --sw-share-tens: 2;
}

.sw-ageing__fill--t3 {
  --sw-share-tens: 3;
}

.sw-ageing__fill--t4 {
  --sw-share-tens: 4;
}

.sw-ageing__fill--t5 {
  --sw-share-tens: 5;
}

.sw-ageing__fill--t6 {
  --sw-share-tens: 6;
}

.sw-ageing__fill--t7 {
  --sw-share-tens: 7;
}

.sw-ageing__fill--t8 {
  --sw-share-tens: 8;
}

.sw-ageing__fill--t9 {
  --sw-share-tens: 9;
}

.sw-ageing__fill--t10 {
  --sw-share-tens: 10;
}

.sw-ageing__fill--u0 {
  --sw-share-units: 0;
}

.sw-ageing__fill--u1 {
  --sw-share-units: 1;
}

.sw-ageing__fill--u2 {
  --sw-share-units: 2;
}

.sw-ageing__fill--u3 {
  --sw-share-units: 3;
}

.sw-ageing__fill--u4 {
  --sw-share-units: 4;
}

.sw-ageing__fill--u5 {
  --sw-share-units: 5;
}

.sw-ageing__fill--u6 {
  --sw-share-units: 6;
}

.sw-ageing__fill--u7 {
  --sw-share-units: 7;
}

.sw-ageing__fill--u8 {
  --sw-share-units: 8;
}

.sw-ageing__fill--u9 {
  --sw-share-units: 9;
}

/* THE BAR'S CAPTION - the share of the cartera that is past due, which is the fourth figure this
 * surface owed the reader and the reason it is here rather than in a fourth card is in
 * AgeingLadderModel.Caption. */
.sw-ageing__caption {
  margin-block-start: var(--space-2);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-primary);
}

/* THE RUNG AS A CONTROL. It is an <a>, so the base stylesheet gives it an underline and the brand
 * colour; both are removed here because the whole rung is the target and the label inside it is not
 * a link within a sentence. The affordance is the surface on hover, which is what every other
 * pressable row in this product uses. */
.sw-ageing__band--control {
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-sm);
  padding-block: var(--space-1);
  padding-inline-end: var(--space-1);
  transition: background-color var(--duration-fast) var(--easing-standard);
}

.sw-ageing__band--control:hover {
  background: var(--color-surface-hover);
}

/* THE FOCUS RING IS THE GLOBAL ONE AND IS NOT RESTATED HERE - base.css draws :focus-visible for
 * every focusable element, and a component redeclaring it is how two rings come to disagree. */

/* THE SELECTED RUNG. The leading border is the ladder's own marker at full strength rather than a
 * new device: every band already carries a 2px inline-start border in --color-border-subtle, so the
 * selected one differs in weight and colour on a line the reader is already reading.
 *
 * TONE IS NOT THE ONLY CARRIER - the rung also prints the word "Filtrando" and announces
 * aria-current="page". Section 9 rule 3, and here it is load-bearing: this is the only thing on the
 * page explaining why the table below is shorter than the figures above describe. */
.sw-ageing__band--selected {
  border-inline-start-color: var(--color-tone-brand-fg);
  background: var(--color-surface-selected);
}

.sw-ageing__selected {
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--letter-spacing-wide);
  text-transform: uppercase;
  color: var(--color-tone-brand-fg);
}

/* A NOTE THAT IS NOT A PANEL — a single self-describing sentence between panels.
 *
 * ADDED 2026-09-05 FOR THE "owes nothing" CASE ON THE CUSTOMER DETAIL, and the reason is measured
 * rather than aesthetic. CustomerDetailLayoutTests holds that a desktop reader must not scroll
 * inside the dialog to see the customer they opened; wrapping one sentence in a panel with a
 * heading put the body over that budget. The sentence names its own subject, so the panel was
 * carrying structure the reader did not need.
 *
 * NOT .sw-panel__intro, which is a panel's opening line and inherits that context. This is a
 * standalone remark, and reusing a component's inner class outside it is how a stylesheet stops
 * describing the markup it is named after. */
.sw-detail__note {
  margin-block: var(--space-3);
  font-size: var(--font-size-sm);
  color: var(--color-text-secondary);
}

/* The cartera summary inside the customer detail. A group rather than a panel — see .sw-detail__note
 * and _CustomerCartera for why the panel wrapper is deliberately absent on both branches. */
.sw-detail__cartera {
  margin-block: var(--space-3);
}

/* ===================================================================================
 * §8.15 THE FLOATING NOTIFICATION — it LOOKS like a toast and does not BEHAVE like one.
 *
 * IT HAS NO TIMER AND WILL NOT GET ONE. That is not a stylistic preference, it is the entire
 * accessibility argument: WCAG SC 2.2.1 is about a time limit "set by the content", and content
 * that never removes itself sets none. The moment a setTimeout appears anywhere near this
 * component, §8.11's refusal of the auto-dismissing kind applies to it and the justification for
 * having it at all is void. NoticeTimerTests asserts it is still on screen after a wait longer
 * than any plausible timer, which is that sentence made mechanical.
 *
 * WHAT IT MUST NEVER CONTAIN: an action. Scott O'Hara's second problem is a control that matters
 * living inside something that removes itself; ours does not remove itself, and the rule is kept
 * anyway because the next person to add a button here will not be reading this file. The dismiss
 * control is not an action on the order - it is the "user-controlled" half of a persistent
 * message, which is what the article recommends in place of a timer.
 *
 * THE MESSAGE ELEMENT IS THE LIVE REGION AND IT IS NEVER DISPLAY:NONE. Assistive technology
 * watches the node it saw at parse time, so the empty state hides the CHROME - padding, border,
 * fill, shadow, the icon and the dismiss control - and leaves the role="status" element in the
 * document at zero size. Hiding the region itself is how a status region stops announcing.
 * =================================================================================== */

/* TOP, AND FIXED — AND BOTH HALVES ARE A DEPARTURE FROM WHERE THIS STARTED, ON MEASUREMENT.
 *
 * IT WAS BOTTOM-STICKY UNTIL 2026-09-08, AND MATERIAL'S SNACKBAR GUIDANCE IS WHY ANYONE WOULD PUT
 * IT THERE. That guidance also says to anchor it so it does not cover navigation controls - and on
 * THIS surface the controls are exactly what the bottom edge holds. Measured as `fixed` at the
 * bottom it covered "Revisar y confirmar", the step's primary action, at 320, 640 and 1280 with no
 * scroll position that freed it; sticky fixed that by reserving its own box at the foot, and the
 * client then reported the sticky version sitting over the next product row while scrolling. The
 * bottom of this page is where the work happens.
 *
 * SO THE CONVENTION IS DEPARTED FROM DELIBERATELY AND FOR A MEASURED REASON RATHER THAN A
 * PREFERENCE. The top of this surface carries no primary action - the topbar is above it and is
 * chrome - which is the whole argument for moving.
 *
 * AND FIXED BECOMES CORRECT AGAIN ONCE IT IS AT THE TOP, which is not a reversion. Sticky was
 * chosen to solve a BOTTOM-EDGE problem: it reserved space at the end of the document so a covered
 * control could be scrolled clear. At the top there is nothing to reserve space for - sticky-top on
 * a first child would push the page header down on every add, which is a layout jump on the reader's
 * every press, and sticky-top on a last child never appears until you scroll to it. `fixed` holds it
 * under the topbar at every scroll position, which is the behaviour wanted.
 *
 * IT IS AN EARLY CHILD OF .sw-page so reading order and tab order match where it is drawn. */
.sw-notice {
  position: fixed;
  z-index: var(--sw-z-notice);

  /* HOW LONG IT STAYS, AS A NAMED TOKEN RATHER THAN A LITERAL — §8.15. order-notice.js READS this
   * value, so the number the reader experiences and the number the bar depletes over are the same
   * one and cannot drift apart. Five seconds is the product owner's, and it is the duration SC
   * 2.2.1's Understanding document uses in its own worked example of a permitted toast. */
  --sw-notice-duration: 5s;

  /* CLEAR OF THE TOPBAR, WHICH IS STICKY AND 64px TALL. The token rather than the number, so a
   * shell that changes height does not leave this overlapping it. */
  inset-block-start: calc(var(--layout-topbar-height) + var(--space-4));
  inset-inline: var(--space-4);
  margin-inline: auto;
  max-inline-size: 32rem;

  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: var(--space-3);

  padding: var(--space-4);
  border: 1px solid var(--color-status-success-fg);
  border-radius: var(--radius-md);
  background: var(--color-status-success-bg);
  box-shadow: var(--shadow-lg);

  color: var(--color-status-success-fg);
  font-size: var(--font-size-sm);
}

/* THE EMPTY STATE, AND `:has` IS DOING THE WORK A SCRIPT WOULD OTHERWISE DO. No class to toggle
 * means no way for the markup and the script to disagree about whether there is a message. */
.sw-notice:has(.sw-notice__message:empty) {
  padding: 0;
  border-width: 0;
  background: none;
  box-shadow: none;

  /* A zero-size box still sits over the page's bottom edge. Nothing may be swallowed by it. */
  pointer-events: none;
}

.sw-notice:has(.sw-notice__message:empty) .sw-notice__icon,
.sw-notice:has(.sw-notice__message:empty) .sw-notice__dismiss {
  display: none;
}

.sw-notice__icon {
  flex: none;
  inline-size: 1.25rem;
  block-size: 1.25rem;
}

/* ONE ICON AT A TIME, CHOSEN BY THE TONE. Both ship in the markup because a <use href> is not
 * something a stylesheet can change, and swapping the icon would mean swapping the wrapper - which
 * holds the live region. Success is the default so a notification that arrives before the script
 * has moved the tone across is not iconless. */
.sw-notice__icon--info { display: none; }

.sw-notice[data-sw-notice-tone=info] .sw-notice__icon--success { display: none; }

.sw-notice[data-sw-notice-tone=info] .sw-notice__icon--info { display: block; }

/* THE INFORMATIONAL TONE, WHICH IS WHAT A REMOVAL TAKES. Section 8.9's four tones, and the choice
 * is argued in OrderCreateViewModel.Notification: a green tick reporting a deletion is a
 * contradiction the reader's eye meets before the sentence. */
.sw-notice[data-sw-notice-tone=info] {
  border-color: var(--color-status-info-fg);
  background: var(--color-status-info-bg);
  color: var(--color-status-info-fg);
}

.sw-notice__message {
  margin: 0;
}

/* §9 rule 6's target, because this is pressed on a phone. */
.sw-notice__dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;

  min-inline-size: var(--layout-touch-target-min);
  min-block-size: var(--layout-touch-target-min);
  margin: calc(var(--space-2) * -1);
  padding: 0;

  border: 0;
  border-radius: var(--radius-sm);
  background: none;

  color: inherit;
  cursor: pointer;
}

.sw-notice__dismiss:hover {
  background: var(--color-surface-hover);
}

/* IT ANIMATES IN, AND §9 RULE 5's GLOBAL BLOCK IN base.css ALREADY SUPPRESSES IT under
 * prefers-reduced-motion - animation-duration is forced to 0.01ms there, for every element, so
 * this needs no query of its own and gets none. Measured rather than assumed: NoticeMotionTests
 * drives Chromium with the media feature emulated and reads the computed duration. */
/* IT SLIDES DOWN FROM THE EDGE IT ENTERS BY, which changed with the placement: the offset was
 * POSITIVE while the component sat at the foot and rose into view. A top-anchored box rising
 * upwards enters from the wrong side of itself. */
@keyframes sw-notice-enter {
  from {
    opacity: 0;
    transform: translateY(calc(var(--space-4) * -1));
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.sw-notice--entering {
  animation: sw-notice-enter var(--duration-base) var(--easing-emphasized);
}




/* THE DEPLETING BAR — §8.15. Movement the client asked for, and the timer's warning: a message that
 * is about to leave says so rather than vanishing unannounced.
 *
 * IT IS A TRANSFORM ON A FULL-WIDTH BAR rather than an animated width, so it runs on the compositor
 * and never lays out. transform-origin is the inline START, so it depletes towards the edge the
 * reader reads away from. */
.sw-notice__progress {
  /* ON THE BOTTOM EDGE RATHER THAN IN A GRID ROW, AND THE DIFFERENCE WAS MEASURED. As a row it
   * added 47px to the box at 320 - a 3px bar plus a row gap plus the grid re-flowing around it -
   * which took the notification from 97px to 144px and put it over "Descartar pedido" in the page
   * header. The obstruction test caught it. Absolute costs nothing: the notification is fixed, so
   * it is already this bar's containing block. */
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;

  block-size: 3px;
  border-end-start-radius: var(--radius-md);
  border-end-end-radius: var(--radius-md);
  background: currentcolor;
  opacity: 0.35;

  transform-origin: left center;
  transform: scaleX(1);
}

.sw-notice:has(.sw-notice__message:empty) .sw-notice__progress {
  display: none;
}

/* IT RUNS ONLY WHILE THE MESSAGE IS ENTERING-AND-STAYING, and it is keyed on the same class the
 * entrance uses so one restart drives both. */
.sw-notice--entering .sw-notice__progress {
  animation: sw-notice-deplete var(--sw-notice-duration) linear forwards;
}

/* PAUSED WITH THE TIMER, and this is the one place the two mechanisms have to agree. The script
 * writes the attribute when it pauses; the bar stops where it is. */
.sw-notice[data-sw-notice-paused] .sw-notice__progress {
  animation-play-state: paused;
}

@keyframes sw-notice-deplete {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

/* UNDER prefers-reduced-motion THE BAR IS STILL THERE AND STILL FULL, and that is a deliberate
 * choice rather than a consequence. §9 rule 5's global block forces every animation to 0.01ms, so a
 * depleting bar would simply snap to empty and read as a broken component. A STATIC full bar keeps
 * the box's shape and says nothing false: the message still leaves on the same timer, and the
 * sentence was announced whole on insertion either way.
 *
 * The rule is written out rather than left to the global block precisely because the global block's
 * effect here would be wrong. */
@media (prefers-reduced-motion: reduce) {
  .sw-notice--entering .sw-notice__progress {
    animation: none;
    transform: scaleX(1);
  }
}
