/*
 * MyPumpGarage — base styles and design tokens.
 *
 * Palette extracted from the v3 prototype, which inherits the nexus·pi
 * identity. Note: the PM's original brief specified black/yellow while v3
 * uses cream/violet/amber — v3 is the version shown to the customer, so it
 * is treated as the source of truth. Pending confirmation (backlog B-015).
 *
 * Tailwind is NOT set up yet: that is Emanuele's task (backlog S-002), and
 * the token names below are the contract it should adopt. This file is
 * hand-written on purpose so the scaffold runs and reads as intentional
 * without a build step. Confirmed as the way forward on 2026-08-06: aligning
 * the design touches this file and barely touches the templates, which is not
 * true of a migration to utility classes.
 *
 * RULE: no magic values in templates. Colours, spacing, radii and shadows
 * come from here. A #5B4B9E written inside a template is a colour nobody
 * will ever be able to change systematically.
 *
 * WHERE THIS DELIBERATELY LEAVES THE PROTOTYPE
 * Two values from v3 do not survive contact with a contrast check, and both
 * are decided rather than drifted:
 *   · amber on white text (2.6:1) → the amber CTA carries near-black text
 *     instead, 5.4:1. Decision of 2026-08-06.
 *   · #A9A398 for secondary labels (2.5:1 on white) → --c-text-muted, 5.0:1.
 * Everything else — the radii, the shadows, the cream table head, the amber
 * active menu item — is the prototype's.
 */

/* ------------------------------------------------------------------ fonts
 * Poppins for the interface, IBM Plex Mono for codes (RT-2026-0041, RIC-0087,
 * serial numbers), as the design system requires. The monospace is not
 * decoration: it makes strings the user reads off a label and types by hand
 * comparable character by character.
 *
 * SERVED BY THE APPLICATION, never from Google Fonts: an on-prem installation
 * may have no internet access (ADR-0009), and a font that fails to load does
 * not break anything visibly — it just changes the whole look of the page,
 * which is the kind of fault nobody reports. Files are vendored by
 * scripts/vendor_assets.sh; versions in static/fonts/VERSIONS.txt.
 *
 * `font-display: swap` and not the default: text is readable immediately in
 * the fallback and reflows once. On a workshop LAN the file arrives in
 * milliseconds, but the ONE case that matters is a cold cache on the shared
 * terminal, where invisible text is a technician standing still.
 */
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/poppins-latin-400-normal.9212f6f9860f.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/poppins-latin-400-italic.a242ba0df3a1.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../fonts/poppins-latin-500-normal.a09f2fccfee3.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/poppins-latin-600-normal.72993dddf88a.woff2") format("woff2");
}
@font-face {
  font-family: "Poppins";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/poppins-latin-700-normal.25b0e113ca7c.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/ibm-plex-mono-latin-400-normal.79936b18df9f.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-mono-latin-600-normal.5a2ce7798732.woff2") format("woff2");
}

:root {
  /* Typography. The fallbacks are not ceremony: they are what the page renders
     with for the first few milliseconds, and on an installation where someone
     has deleted static/fonts/ they are what it renders with for good. */
  --font-ui:   "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* Surfaces */
  --c-bg:            #F7F4EF;
  --c-surface:       #FFFFFF;
  /* Cream tint under a table head — v3 uses it to separate the head from the
     rows without a heavy rule. */
  --c-surface-alt:   #FBF9F5;
  --c-border:        #ECE7DF;
  /* Between rows, lighter than a contour: a full --c-border on every row of a
     sixteen-row table draws a grid, and the eye then reads the lines instead of
     the data. */
  --c-border-soft:   #F5F1EA;
  /* A mark that has to be seen on cream — the menu dots, the dashed avatar of
     an unassigned order. --c-border disappears against the surface. */
  --c-border-strong: #D8D2C6;

  /* Text */
  --c-text:          #333333;
  --c-text-strong:   #2B2B2B;
  --c-text-muted:    #6B6B6B;

  /* Brand */
  --c-primary:       #5B4B9E;
  /* Pressed and hovered primary. v3 uses this darker violet for its main CTA;
     here it is the hover, so the resting button stays the documented brand
     colour and the interaction still has somewhere to go. */
  --c-primary-strong: #4F3E8E;
  --c-primary-soft:  #F3EDFB;
  --c-accent:        #D9912C;
  --c-accent-soft:   #FBF1E2;
  /* Amber wash of the current menu item — v3's #FBEED3, one step warmer than
     --c-accent-soft so «current» and «hovered» are not the same colour. */
  --c-accent-tint:   #FBEED3;
  /* Amber that can carry text. --c-accent on --c-accent-tint is 2.5:1; this is
     4.9:1 on the same background. Same reasoning as --c-warn-strong, and the
     same value: one is a status, the other is the brand accent, and a token
     that means two things gets used for the wrong one. */
  --c-accent-strong: #8A5F0D;
  /* Hover of the amber CTA. */
  --c-accent-deep:   #C4831F;
  /* Under the cursor, on a row. v3's #FBF6EC: warm, and light enough that the
     status pills keep their contrast on top of it. */
  --c-row-hover:     #FBF6EC;

  /* Workflow status — the mapping status→token lives in one place only.
     A template writing a raw colour for "Riparato" duplicates a rule. */
  --c-ok:            #1F7A3A;
  --c-ok-soft:       #E6F4EA;
  /* Hover of the green action button, the same relationship --c-danger-deep has
     with --c-danger. Declared 2026-08-24 with «Ingresso merce», the prototype's
     one green CTA (#3F8A46 there; this is v3's own green, because two
     near-identical greens on one screen are how a product stops looking like
     one). 7.6:1 with white on it. */
  --c-ok-deep:       #17612E;
  --c-warn:          #A56A12;
  --c-warn-soft:     #FBF1E2;
  /* --c-warn on --c-warn-soft is 4.0:1, below the 4.5:1 required on text.
     This darker amber is the one used for status text and figures. */
  --c-warn-strong:   #8A5F0D;
  --c-danger:        #A3272F;
  /* Hover of the destructive button, the same relationship --c-accent-deep has
     with --c-accent. 8.4:1 with white on it, so the label of the one button
     that must be read before it is pressed does not weaken under the cursor. */
  --c-danger-deep:   #8C212A;
  --c-danger-soft:   #FBE9EA;
  --c-info:          #3A5B9E;
  --c-info-soft:     #E9EEF9;
  --c-neutral:       #6B6B6B;
  --c-neutral-soft:  #F1EEE8;

  /* Spacing scale */
  --s-1: 0.25rem;  --s-2: 0.5rem;   --s-3: 0.75rem;
  --s-4: 1rem;     --s-6: 1.5rem;   --s-8: 2rem;    --s-12: 3rem;

  /* Radii — the four the prototype actually uses: 8 on pills and small
     controls, 10 on buttons and inputs, 14 on cards, 16 on a dialog. */
  --r-sm: 8px;  --r-md: 10px;  --r-lg: 14px;  --r-xl: 16px;  --r-pill: 999px;

  /* Elevation. The card shadow is v3's, and it is what replaces the border:
     the prototype's cards have no contour at all — they lift off the cream
     instead. Warm-tinted, not grey, or it reads as dirt on a cream page. */
  --shadow-1: 0 2px 10px rgba(60, 50, 30, 0.06);
  --shadow-2: 0 12px 34px rgba(43, 43, 43, 0.14);
  /* A primary action is raised. Tinted with its own colour, as in v3 — a
     neutral grey shadow under a violet button looks like a rendering bug. */
  --shadow-cta-primary: 0 3px 8px rgba(79, 62, 142, 0.30);
  --shadow-cta-accent:  0 3px 8px rgba(217, 145, 44, 0.35);

  /* The dialog floats higher than a card and needs to say so without turning
     into a drop shadow from 2012. Two layers: a wide, very soft one that lifts
     the whole panel off the page, and a tight one just under its edge that
     keeps the corner from dissolving. Warm-tinted like the others, because a
     neutral grey shadow on a cream page reads as dirt. */
  --shadow-3: 0 32px 64px -16px rgba(43, 38, 28, 0.26),
              0 8px 20px -8px rgba(43, 38, 28, 0.14);

  /* Scrim behind a modal dialog. A token because ::backdrop is styled in one
     place but read in every modal. Warm rather than neutral — it dims the cream
     page instead of greying it — and lighter than it used to be (0.55) because
     it is now paired with a blur: two ways of pushing the page back at once
     make the content behind unreadable, which is a different thing from
     inactive. */
  --c-scrim: rgba(38, 33, 27, 0.42);

  /* Motion. TWO curves and TWO durations for the whole application: a system
     with a curve per component has no motion language, it has decoration.

     `--ease-out` is the curve of the iOS layer (`--ease-ios` in ios.css), kept
     identical on purpose so S-010 does not change how things move — only how
     they look. Entering is slower than leaving, as everywhere: an element
     arriving is information, an element leaving is already understood. */
  --t-fast: 120ms ease;
  --ease-out: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-in:  cubic-bezier(0.4, 0, 0.9, 1);
  --t-enter: 260ms;
  --t-leave: 150ms;

  /* Minimum touch target. Not a suggestion: the technician wears gloves. */
  --touch-min: 44px;

  /* Height of the sticky top bar. A token because TWO things read it: the bar
     itself, and the side navigation that has to start exactly below it. Two
     hand-written 58px would agree until one of them changed. */
  --header-h: 58px;

  /* Width of the docked side navigation. Read by the aside and by nothing
     else today, but it is the number to change when a label no longer fits. */
  --sidenav-w: 15rem;
}

/* Alpine hides an element carrying x-cloak until it has taken over, and needs
   this rule to do it — the library ships no CSS. Nothing uses x-cloak right now
   (the drawer's scrim was the candidate and ended up hiding itself in plain CSS,
   which is better), but the rule stays: without it, an `x-cloak` added later
   does NOTHING AT ALL, silently, and the flash it was meant to prevent stays.
   A no-op attribute is a worse trap than an unused line. */
[x-cloak] { display: none !important; }

*, *::before, *::after { box-sizing: border-box; }

/* 14px and not 16px, and it is the design's density, not a squeeze: the v3
   back-office sets body text at 13.5–14px because these screens are read at a
   desk with the whole workshop on one page. The terminal is the opposite case
   and overrides it — `.terminal` is 18px — which is exactly why the two layouts
   are separate files.

   The spacing scale is unaffected: --s-* are rem, and rem is the root font
   size, not this one. */
body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.5;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  /* Poppins is geometric: without this its default figures are fine but its
     kerning at small sizes is loose. */
  -webkit-font-smoothing: antialiased;
  /* ⚠ DICHIARATO DAL 2026-08-07, e non e' pedanteria: questo foglio non e' piu'
     solo sulla pagina. `app.built.css` gli sta accanto (vedi base.html) e il suo
     `@layer base` dice `letter-spacing: -0.2px` su body — una regola in un layer
     perde contro questo file, che non e' stratificato, ma SOLO sulle proprieta'
     che questo file nomina. Su quelle che taceva vinceva contro il foglio di
     default del browser, ed ereditava su tutto il testo dell'applicazione.
     Misurato: 4% dei pixel diversi su ogni schermata, senza spostare nulla.
     Sparisce con app.css quando S-010 lo ritira, e a quel punto la crenatura
     stretta di Inter e' quella giusta. */
  letter-spacing: normal;
}

h1, h2, h3 { color: var(--c-text-strong); line-height: 1.25; font-weight: 700; }
h1 { font-size: 1.375rem; margin: 0 0 var(--s-2); }   /* 22px, as v3's page titles */
h2 { font-size: 1rem;     margin: 0 0 var(--s-3); }   /* 16px, as v3's panel headings */

/* `font-variant-ligatures` per lo stesso motivo del `letter-spacing` su body:
   lo strato iOS lo spegne sui codici, e finche' i due fogli convivono la scelta
   di questo deve essere scritta invece che sottintesa. */
code, .mono { font-family: var(--font-mono); font-size: 0.92em; font-variant-ligatures: normal; }

a { color: var(--c-primary); }

/* Focus must always be visible — every flow is completable by keyboard,
   and half the technicians use a QR reader that sends Enter. */
:focus-visible {
  outline: 3px solid var(--c-accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

.skip-link {
  position: absolute; left: -9999px;
  background: var(--c-surface); padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md); z-index: 100;
}
.skip-link:focus { left: var(--s-4); top: var(--s-4); }

/* ---------------------------------------------------------------- layout */

.app-header {
  display: flex; align-items: center; gap: var(--s-4);
  padding: 0 var(--s-6); height: var(--header-h); flex-shrink: 0;
  background: var(--c-surface); border-bottom: 1px solid var(--c-border);
  position: sticky; top: 0; z-index: 20;
}
.app-header__brand {
  font-size: 1.375rem; font-weight: 700; letter-spacing: -0.5px;
  color: var(--c-text-strong); text-decoration: none;
  white-space: nowrap;
}
.app-header__brand-accent { color: var(--c-primary); }
.app-header__spacer { flex: 1; }

/* ------------------------------------------------------------- user chip
 * Who is signed in, and the way out. Top right, as in the design.
 *
 * It is here rather than in a page because it answers a question the reader has
 * on every screen — on a shared workstation the previous person's session is
 * the normal state, not the exception, and until this existed the application
 * had a way in and no way out at all.
 *
 * The panel is hidden IN CSS and shown by a class Alpine binds, not with
 * x-show: same reason as the drawer scrim, whose comment records what x-show
 * cost when it was tried there.
 */
.user-chip { position: relative; flex-shrink: 0; }

.user-chip__button {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: var(--touch-min);
  padding: var(--s-1) var(--s-2);
  background: transparent; border: 1px solid transparent;
  border-radius: var(--r-pill);
  font: inherit; color: var(--c-text-strong);
  cursor: pointer;
}
.user-chip__button:hover { background: var(--c-bg); }

/* The initial in the operator's own colour, the same treatment as the avatar in
   the register — one person is one colour everywhere in the product. */
.user-chip__avatar {
  width: 34px; height: 34px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  background: var(--c-primary);
  font-size: 0.9rem; font-weight: 700; color: #fff;
}
.user-chip__name { font-weight: 500; white-space: nowrap; }
/* The caret is decoration; it rotates so the closed and open states differ by
   more than the panel being there. */
.user-chip__caret { font-size: 0.6rem; color: var(--c-text-muted); transition: transform var(--t-fast); }
.user-chip--open .user-chip__caret { transform: rotate(180deg); }

.user-chip__menu {
  display: none;
  position: absolute; top: calc(100% + var(--s-2)); right: 0;
  z-index: 40;
  min-width: 15rem;
  padding: var(--s-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
}
.user-chip__menu--open { display: block; }

.user-chip__who { margin: 0; font-weight: 600; color: var(--c-text-strong); }
.user-chip__meta {
  margin: 0 0 var(--s-3);
  font-size: 0.8rem; color: var(--c-text-muted);
}

/* The one destination in the menu. A full-width row and not an inline link: it
   sits between two blocks in a panel that is also touched with a finger, and a
   text-sized target here would be the smallest one in the whole application. */
.user-chip__link {
  display: flex; align-items: center;
  min-height: var(--touch-min);
  margin: 0 calc(var(--s-3) * -1) var(--s-2);
  padding: 0 var(--s-3);
  border-top: 1px solid var(--c-border);
  color: var(--c-text-strong); text-decoration: none;
}
.user-chip__link:hover { background: var(--c-bg); color: var(--c-primary); }

.user-chip__form { margin: 0; }
.user-chip__logout { width: 100%; }

/* ---------------------------------------------------- centro notifiche
 * RF-070, RF-071 — ADR-0007, 2026-08-26.
 *
 * Il campanello del prototipo v3, a sinistra di chi è connesso. Vestito qui e
 * non nello strato iOS per la ragione di ogni riga di questo foglio: sta
 * nell'intestazione, e l'intestazione è ancora v3. Sono righe che S-010 dovrà
 * portare, ed è dichiarato.
 *
 * Il pannello è nascosto in CSS e mostrato da una classe che Alpine lega, non
 * con x-show: stessa scelta del .user-chip qui sopra e dello sfondo del
 * cassetto, i cui commenti registrano cosa costò x-show quando fu provato.
 */
.notif { position: relative; flex-shrink: 0; }

/* Quadrato come il tasto ingranaggio dei registri e non pillola come il
   .user-chip accanto: qui non c'è un'etichetta di testo da abbracciare, e un
   bersaglio tondo intorno a una sola icona ne sposta il centro ottico. */
.notif__bell {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--touch-min); height: var(--touch-min);
  background: transparent; border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--c-text-muted);
  cursor: pointer;
}
.notif__bell:hover { background: var(--c-bg); color: var(--c-text-strong); }
/* Aperto, il tasto resta acceso: senza, il pannello sembra appartenere alla
   pagina invece che al controllo che lo ha aperto. */
.notif--open .notif__bell { background: var(--c-bg); color: var(--c-text-strong); }

/* currentColor, quindi la campana segue il tasto in tutti e tre gli stati senza
   una seconda regola. È la ragione per cui lo sprite non fissa colori. */
.notif__icon { display: block; }

/* IL CONTATORE, e questo elemento è il bersaglio del polling: HTMX lo sostituisce
   ogni trenta secondi con sé stesso. Posizionato lui e non la pastiglia dentro,
   così quando non ci sono non lette il frammento è vuoto e non occupa nulla —
   niente scatoletta trasparente in un angolo del tasto. */
.notif__count {
  position: absolute; top: 3px; right: 3px;
  display: block;
}

/* Testo scuro sull'ambra e non bianco, ed è la decisione del 2026-08-06 già in
   esercizio su .button--accent: bianco su --c-accent sta a 2.5:1, questo a
   6.5:1. Una pastiglia da 0.66rem è esattamente la misura in cui un contrasto
   scarso diventa illeggibile invece di essere solo brutto. */
.notif__pill {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: var(--r-pill);
  background: var(--c-accent); color: var(--c-text-strong);
  font-size: 0.66rem; font-weight: 700; line-height: 1;
}

.notif__panel {
  display: none;
  position: absolute; top: calc(100% + var(--s-2)); right: 0;
  z-index: 40;
  width: 24rem;
  /* Su schermo stretto un pannello ancorato a destra e larghezza fissa spinge la
     pagina di lato invece di stare dentro. Stessa disciplina del min-width: 0
     più sotto: nessun elemento dell'intestazione allarga il documento. */
  max-width: calc(100vw - var(--s-6) * 2);
  background: var(--c-surface);
  border: 1px solid var(--c-border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
.notif__panel--open { display: block; }

/* Il titolo sta nel guscio e NON nel frammento: resta a schermo mentre il corpo
   si ricarica. Un titolo dentro il bersaglio dello swap sparirebbe e
   ricomparirebbe a ogni apertura. */
.notif__title {
  margin: 0;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--c-border-soft);
  font-size: 0.85rem; font-weight: 600; color: var(--c-text-strong);
}

/* «Segna tutte come lette» sta nel corpo perché deve sparire quando non c'è
   niente da segnare, e il corpo è la parte che si ricarica. */
.notif__actions {
  display: flex; justify-content: flex-end;
  padding: var(--s-2) var(--s-4);
  border-bottom: 1px solid var(--c-border-soft);
}
.notif__readall {
  min-height: var(--touch-min);
  padding: 0 var(--s-2);
  background: none; border: 0;
  font: inherit; font-size: 0.75rem; font-weight: 600;
  color: var(--c-primary-strong);
  cursor: pointer;
}
.notif__readall:hover { color: var(--c-primary); text-decoration: underline; }

/* L'altezza massima è del corpo scorrevole e non del pannello: il titolo e la
   fascia del taglio devono restare fermi mentre le righe scorrono. */
.notif__list { max-height: 23rem; overflow-y: auto; }

/* Un <button> a piena larghezza, e la ragione è la riga della dashboard: un div
   cliccabile è un elemento che riceve il fuoco senza dire cosa fa. Qui sono
   venti comandi in colonna, e senza <button> nessuno sarebbe raggiungibile col
   tabulatore. Il reset è quindi obbligatorio, non estetico: un button eredita
   font, centratura e bordo dal browser. */
.notif__row {
  display: flex; align-items: flex-start; gap: var(--s-3);
  width: 100%;
  padding: var(--s-3) var(--s-4);
  background: var(--c-surface); border: 0;
  border-bottom: 1px solid var(--c-border-soft);
  font: inherit; text-align: left;
  cursor: pointer;
}
.notif__row:hover { background: var(--c-row-hover); }
/* Non letta: il fondo caldo del prototipo v3 (#FBF6EC), che è lo stesso token
   dell'evidenziazione di riga — le pastiglie di stato ci tengono il contrasto
   sopra, ed è per questo che quel token esiste. */
.notif__row--unread { background: var(--c-row-hover); }

/* La pallina. Spenta è un grigio che si vede sul bianco; accesa prende il colore
   dello stato d'arrivo dalle stesse variabili della pastiglia della dashboard —
   uno stato è un colore solo in tutto il prodotto.
   PIENA e non a contorno: a 8px un anello di un pixel e mezzo è un alone, la
   stessa ragione per cui il pallino della scadenza è l'unico riempimento dello
   sprite. */
.notif__dot {
  flex-shrink: 0;
  width: 8px; height: 8px; margin-top: 6px;
  border-radius: var(--r-pill);
  background: var(--c-border-strong);
}
.notif__row--unread .notif__dot { background: var(--status-fg); }

/* min-width: 0 è portante e non ordine: è un figlio flex, e il suo
   min-width: auto di default rifiuta di stringersi sotto il proprio contenuto —
   quindi il troncamento di .notif__detail non avverrebbe e la riga allargherebbe
   il pannello. */
.notif__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.notif__headline {
  display: flex; align-items: center; gap: var(--s-2);
  font-size: 0.8rem; color: var(--c-text-strong);
}
.notif__arrow { color: var(--c-text-muted); }
.notif__status { font-weight: 600; color: var(--status-fg); }

/* Una riga sola: la nota di una transizione può essere un paragrafo, e venti
   paragrafi in un pannello alto 23rem sono due notifiche visibili. */
.notif__detail {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 0.75rem; color: var(--c-text-muted);
}
.notif__meta { font-size: 0.7rem; color: var(--c-text-muted); }

/* Lo stato vuoto e il taglio dichiarato: due frasi, stesso trattamento, perché
   dicono la stessa classe di cosa — dove finisce ciò che questo pannello sa. */
.notif__empty {
  margin: 0;
  padding: var(--s-6) var(--s-4);
  text-align: center;
  font-size: 0.78rem; color: var(--c-text-muted);
}
.notif__cut {
  margin: 0;
  padding: var(--s-2) var(--s-4);
  border-top: 1px solid var(--c-border-soft);
  background: var(--c-surface-alt);
  font-size: 0.7rem; color: var(--c-text-muted);
}

/* `min-width: 0` is load-bearing, not tidiness: this is a flex item, and a flex
   item's default `min-width: auto` refuses to shrink below its content. A wide
   data table inside it would then push the whole page sideways instead of
   scrolling inside its own .table-scroll container.

   🔄 `max-width: 72rem` E `margin: 0 auto` SONO USCITI IL 2026-08-26, su
   richiesta del committente e guardando lo schermo: su 1920px la colonna si
   fermava a 1152px e lasciava ~270px vuoti per lato, con la tabella degli ordini
   che scorreva dentro il proprio contenitore mentre accanto c'era spazio libero.
   Una misura di leggibilità presa dalla prosa applicata a una schermata che è
   quasi tutta tabella: la riga ha nove colonne, e lo spazio in più diventa
   colonne che si vedono senza trascinare.

   ⚠️ Ne segue che su un monitor molto largo le righe diventano lunghe: è la
   contropartita accettata. Se un giorno dà fastidio, il rimedio NON è rimettere
   una misura a `.app-main` — che riporterebbe il vuoto su 1920px — ma darla alle
   schermate di sola prosa, che sono l'unico posto dove serviva. */
.app-main { flex: 1; min-width: 0; padding: var(--s-6); width: 100%; }

/* ------------------------------------------------- shell: aside + content */
/* The row that holds navigation and page side by side. The aside is OUTSIDE
   .app-main and stays a column of its own: the menu is anchored to the left
   edge and the content takes everything that is left. Dal 2026-08-26 «tutto
   ciò che resta» è letterale — `.app-main` non ha più una larghezza massima. */
.app-shell { display: flex; flex: 1; align-items: stretch; }

.app-footer {
  display: flex; gap: var(--s-4); align-items: center;
  padding: var(--s-4) var(--s-6);
  color: var(--c-text-muted); font-size: 0.85rem;
  border-top: 1px solid var(--c-border);
}
.app-footer__version { margin-left: auto; font-family: ui-monospace, monospace; }

.stack { display: flex; flex-direction: column; gap: var(--s-6); }

/* No contour: in v3 a card lifts off the cream with a shadow and has no border
   at all. A card that has both reads as a box drawn twice. */
.card {
  background: var(--c-surface);
  border-radius: var(--r-lg); padding: var(--s-6); box-shadow: var(--shadow-1);
}

.lede { font-size: 1.05rem; margin: 0 0 var(--s-3); }
.muted { color: var(--c-text-muted); }

/* La macchina a stati come la disegna la pagina di ingresso: UNA RIGA PER
   STATO, «da qui si va a». Fino al 2026-08-26 era una fila sola che andava a
   capo, e una fila racconta una sequenza — che è il modo in cui «Non
   riparabile», che è terminale, finiva in mezzo alla strada verso «Riparato».
   Righe distinte non possono dirlo. Le pastiglie sono `.status-pill` con la
   mappa `status--*`: gli stessi colori della dashboard, non colori suoi. */
.flow { display: flex; flex-direction: column; gap: var(--s-3); list-style: none; padding: 0; margin: 0 0 var(--s-4); }
.flow li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.flow__arrow { color: var(--c-text-muted); }
.flow__terminal { font-size: 0.78rem; color: var(--c-text-muted); }

/* Le porte della pagina di ingresso — le schermate che chi guarda può aprire,
   da `nav_sections`. Una fila che va a capo e non una griglia: quante sono
   dipende da chi guarda, e una griglia con una porta sola disegna una colonna
   vuota accanto. */
.doors { display: flex; flex-wrap: wrap; gap: var(--s-3); }

/* 🔄 `.checklist` è uscita il 2026-08-26 con l'elenco di backlog della home, che
   era il suo unico uso. Una classe senza usi è una classe che il prossimo
   riutilizza per un'altra cosa. */

/* --------------------------------------------------------------- badges */

.badge {
  display: inline-flex; align-items: center;
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-pill);
  font-size: 0.72rem; font-weight: 600; white-space: nowrap;
}
.badge--neutral { background: var(--c-neutral-soft); color: var(--c-neutral); }
.badge--muted   { background: var(--c-primary-soft); color: var(--c-primary); }
.badge--primary { background: var(--c-primary-soft); color: var(--c-primary); }
.badge--info    { background: var(--c-info-soft);    color: var(--c-info); }
.badge--warn    { background: var(--c-warn-soft);    color: var(--c-warn); }
.badge--ok      { background: var(--c-ok-soft);      color: var(--c-ok); }
.badge--danger  { background: var(--c-danger-soft);  color: var(--c-danger); }

/* -------------------------------------------------------------- controls */

.button {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: var(--touch-min); min-width: var(--touch-min);
  padding: 0 var(--s-6);
  border: 1px solid var(--c-border); border-radius: var(--r-md);
  background: var(--c-surface); color: var(--c-text-strong);
  font: inherit; font-weight: 600; text-decoration: none; white-space: nowrap;
  cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast);
}
.button:hover { border-color: var(--c-primary); }
.button:disabled, .button[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

.button--primary {
  background: var(--c-primary); border-color: var(--c-primary); color: #fff;
  box-shadow: var(--shadow-cta-primary);
}
.button--primary:hover { background: var(--c-primary-strong); border-color: var(--c-primary-strong); }

/* THE AMBER CTA — «+ Nuovo …» on every register, «Stampa etichetta» later.
 * Near-black label and not white: white on --c-accent is 2.6:1 and the label of
 * the one button the screen is built around is the last thing that may become
 * hard to read. This is 5.4:1, keeps v3's amber exactly, and is the same
 * treatment as the amber badges of the platform this design comes from.
 * Decision of 2026-08-06. */
.button--accent {
  background: var(--c-accent); border-color: var(--c-accent); color: var(--c-text-strong);
  box-shadow: var(--shadow-cta-accent);
}
.button--accent:hover { background: var(--c-accent-deep); border-color: var(--c-accent-deep); }

/* The destructive action. Its own hover, and it is a fix rather than polish:
   without it `.button:hover` won on specificity and drew a VIOLET border around
   a red button — the brand colour announcing the one act nobody should confirm
   by mistake. Visible on «Sospendi» from the day the register existed. */
.button--danger { background: var(--c-danger); border-color: var(--c-danger); color: #fff; }
.button--danger:hover { background: var(--c-danger-deep); border-color: var(--c-danger-deep); }

/* «Ingresso merce», 2026-08-24 — la SECONDA azione di una testata, quella che
   non è ambra. Il prototipo mette due tasti pieni accanto al titolo del
   magazzino e li distingue per colore anziché per peso, perché non sono la
   stessa cosa: «+ Nuovo ricambio» crea una riga in anagrafica, «Ingresso merce»
   muove il numero che l'officina crede vero. Due ambre affiancate sarebbero due
   azioni principali, cioè nessuna.

   ⚠️ Il verde non è decorativo ed è l'unico posto in cui questo prodotto lo usa
   per un'AZIONE: altrove `--c-ok` veste uno stato («Riparato», «Disponibile»).
   La differenza è che qui il verde dice «entra merce», che è la stessa direzione
   che dice negli stati — qualcosa che va bene — e non un secondo significato.
   Bianco su --c-ok è 5.9:1, quindi l'etichetta regge anche sotto il cursore. */
.button--ok { background: var(--c-ok); border-color: var(--c-ok); color: #fff; }
.button--ok:hover { background: var(--c-ok-deep); border-color: var(--c-ok-deep); }

/* Quiet action: «Annulla» beside a save, «Torna indietro». It is a real button
   with a real touch target — only its skin is quiet, because the thing that
   makes a cancel safe is that it is easy to hit, not that it is easy to see. */
.button--quiet {
  background: transparent; border-color: transparent; color: var(--c-text-muted);
  font-weight: 500; text-decoration: underline;
}
.button--quiet:hover { border-color: transparent; color: var(--c-text-strong); }

/* ----------------------------------------------------------------- forms */

.field { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-4); }
.field__label { font-weight: 600; font-size: 0.9rem; color: var(--c-text-strong); }
.field__required { color: var(--c-danger); margin-left: 2px; }
/* ⛔ `.field__help` SI È TRASFERITO — 2026-08-07.
   L'aiuto di un campo non sta più sotto di esso: sta dietro un «?» accanto
   all'etichetta → `components/_info_helper.html`, vestito da
   `static/css/components.css`. Sette righe di testo grigio tutte visibili
   insieme non erano sette aiuti, erano rumore.
   Qui non resta niente: la classe non è più resa da nessun template. */
.field__errors { margin: 0; padding-left: var(--s-4); color: var(--c-danger); font-size: 0.85rem; }

/* One rule for every form control in the application: the filter bar joins
   the list instead of restating the same six declarations. */
/* 1.5px and --c-border-strong, both from v3: on a white card a 1px hairline in
   the page's border colour makes the edge of the field a guess, and a field
   whose edge is a guess gets clicked next to instead of in. */
/* `.palette-trigger` è in questo elenco e non nel suo componente, ed è una
   decisione: il grilletto della paletta dei colori non deve SOMIGLIARE ai
   controlli che gli stanno accanto nella riga — deve essere lo stesso controllo.
   Ci sta dentro anche perché è il verso giusto per S-010: quando quest'elenco
   verrà riscritto nei token iOS, il grilletto si sposta con i suoi vicini invece
   di restare indietro con il bordo di un'altra epoca. Il suo contenuto — la
   pastiglia, il nome, il chevron — sta in `components.css`. */
.field input, .field select, .field textarea, .filters__control, .field .palette-trigger {
  min-height: var(--touch-min);
  padding: var(--s-2) var(--s-3);
  border: 1.5px solid var(--c-border-strong); border-radius: var(--r-md);
  background: var(--c-surface); color: var(--c-text); font: inherit;
  width: 100%;
}
/* A field that holds a code — a badge, a serial — asks for the monospace: the
   difference between 0 and O is then visible while typing, not after saving.
   Opt-in with class="mono" on the input, as v3 does on matricola and telefono. */
.field input.mono, .filters__control.mono { font-family: var(--font-mono); }
.field--invalid input, .field--invalid select, .field--invalid textarea,
.field--invalid .palette-trigger { border-color: var(--c-danger); }

/* ------------------------------------------------------- states & toasts */

.empty-state, .error-state {
  padding: var(--s-8); text-align: center;
  border: 1px dashed var(--c-border); border-radius: var(--r-lg);
}
.empty-state__message { color: var(--c-text-muted); margin: 0 0 var(--s-4); }
/* La seconda riga di uno stato vuoto: perche' e' vuoto, non che lo e'. La usa la
   ricerca in dashboard — «puo' essere gia' stato consegnato» — dove la sola
   frase «nessun risultato» si legge come «non esiste». */
.empty-state__hint {
  margin: calc(var(--s-4) * -1) 0 var(--s-4);
  font-size: 0.85rem; color: var(--c-text-muted);
}
.error-state { border-color: var(--c-danger); background: var(--c-danger-soft); }
.error-state__message { color: var(--c-danger); margin: 0 0 var(--s-4); font-weight: 600; }

.toasts {
  position: fixed; bottom: var(--s-6); right: var(--s-6);
  display: flex; flex-direction: column; gap: var(--s-2);
  z-index: 60; pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-left: 4px solid var(--c-ok);
  border-radius: var(--r-md); box-shadow: var(--shadow-2);
  pointer-events: auto;
}
.toast--warn   { border-left-color: var(--c-warn); }
.toast--danger { border-left-color: var(--c-danger); }
.toast__close {
  margin-left: auto; background: none; border: none; cursor: pointer;
  font-size: 1.25rem; line-height: 1; color: var(--c-text-muted);
  min-width: var(--touch-min); min-height: var(--touch-min);
}

/* HTMX shows elements with .htmx-indicator only while a request is in
   flight. Mandatory on every network action: server-rendered HTML means
   waiting, and waiting without feedback is a broken interface — the
   technician will press the button three times. */
.htmx-indicator { visibility: hidden; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { visibility: visible; }

/* -------------------------------------------- il terminale d'officina
   ADR-0015, 2026-08-26.

   🔄 QUI C'ERA IL GUSCIO DELLE CINQUE SCHERMATE PIENE — `.terminal__header`,
   `.terminal__brand`, `.terminal__installation`, `.terminal__main` e le due
   regole che ingrandivano tasti e campi dentro `.terminal`. Vestivano
   `templates/base_terminal.html`, che non esiste più: il terminale è una
   schermata del back-office e usa il guscio di tutte le altre. Il blocco è
   stato SOSTITUITO e non annotato accanto, perché nessuna delle sue regole
   aveva più un elemento da vestire.

   ⛔ E CON ESSO È SPARITO `static/css/terminal.css`, il foglio non stratificato
   del 2026-08-25: esisteva per riconquistare quattro regole di base che questo
   foglio batteva dentro `.terminal`, e senza `.terminal` non c'era più niente
   da riconquistare. Anche il blocco `.terminal*` di `ios.css` è andato via.

   ⚠️ QUELLO CHE RESTA DI R-018 È IL VINCOLO DI PRODOTTO, e sta in due regole di
   questo blocco: `.terminal-panel__leap` è alto **68px** — la misura che
   ripresa.md §14 nomina, un bersaglio per una mano col guanto — e la casella di
   ricerca è alta 56px. Sono le due cose che compensano la densità del
   back-office su una postazione che si usa in piedi: non si tolgono per
   uniformare. */

/* La casella: è il lettore, quindi resta il campo più grande della schermata —
   ma non prende più tutta la riga.

   🔄 ERA `flex: 1 1 100%` E ALTA 56px, richiesta del committente il 2026-08-26:
   «il campo di ricerca fallo più piccolo». ⚠️ ADR-0015 nominava quei 56px fra le
   due misure che compensano la densità del back-office: adesso sono **48**, che
   resta sopra `--touch-min` (44px) — sotto quella soglia non si scende, ed è la
   ragione per cui non è diventata una casella qualunque. I 68px dei tasti
   «Avanza a», che sono l'altra misura, non si sono mossi.

   `max-width` e non una colonna di `.filters`: accanto ci sta il pulsante della
   fotocamera, e i due insieme non devono arrivare al bordo dello schermo. */
.terminal-search__field { flex: 1 1 20rem; max-width: 32rem; min-width: 0; }
.terminal-search__control {
  min-height: 48px;
  font-size: 1rem;
  padding-inline: var(--s-3);
}

/* ---- «Scansiona QR asset»: il pulsante della scansione ------------------ */
/* Scuro come nel prototipo, e la ragione non è estetica: accanto a una casella
   chiara è l'unica cosa premibile della riga, e non deve competere col viola dei
   tasti che avanzano lo stato — quelli sono atti sul dominio, questo apre un
   ingresso.

   🔄 NON È PIÙ «IL PULSANTE DELLA FOTOCAMERA» — ADR-0022, 2026-08-31: apre
   l'ingresso giusto per il dispositivo, il lettore da PC e la fotocamera da
   mobile. ⛔ E la condizione che lo faceva comparire non c'è più: adesso c'è
   sempre, perché una via praticabile c'è sempre. La regola di CLAUDE.md si è
   spostata sul ripiego dentro il riquadro (`.qr-overlay__swap`), e vive in
   `terminal/pages/terminal.html` — non qui. */
.terminal-search__scan {
  appearance: none; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 48px; padding: 0 var(--s-6);
  border-radius: var(--r-md);
  background: var(--c-text-strong); color: #fff;
  font: inherit; font-weight: 700; white-space: nowrap;
  box-shadow: 0 3px 8px rgba(43, 43, 43, 0.28);
}
.terminal-search__scan:hover { background: #1C1C1C; }
.terminal-search__scan:active { transform: scale(0.98); }
.terminal-search__scan-icon { font-size: 1.15rem; line-height: 1; }

/* ---- «+ Nuovo asset»: l'accettazione, dal terminale (ADR-0021) --------- */
/* ⛔ NON UNA VARIANTE DI COLORE: è `.button--accent`, l'ambra che ogni «+ Nuovo
   …» dell'applicazione ha già, e cambiarla qui vorrebbe dire che lo stesso atto
   ha due colori a seconda della schermata. L'unica cosa che questa regola fa è
   l'ALTEZZA: `.button` è alto quanto basta a una riga di testo, e accanto a una
   casella e a un tasto da 48px sarebbe più basso di tutti e due. ⚠️ E 48px non è
   un pareggio estetico — è la misura che ADR-0015 nomina fra quelle che
   compensano la densità del back-office su un touch screen, sopra `--touch-min`
   (44px). Sotto quella soglia non si scende. */
.terminal-search__new { min-height: 48px; }

/* ---- l'inquadratura ----------------------------------------------------- */
/* ⛔ NON È IL DIALOGO DELL'APPLICAZIONE, e non deve diventarlo: quello serve ai
   frammenti che manda il server. Qui c'è un flusso video da spegnere, e il
   `z-index` sta sotto il `<dialog>` nativo — che vive nel top layer e non ha un
   indice — quindi un pannello aperto resta comunque sopra. */
.qr-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-4);
  background: rgba(20, 14, 45, 0.72);
}
.qr-overlay__panel {
  display: flex; flex-direction: column; gap: var(--s-3);
  width: 100%; max-width: 34rem;
  padding: var(--s-6);
  background: var(--c-surface);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-2);
}
/* ⭐ IL TITOLO DEL RIQUADRO — ADR-0022. I due riquadri hanno lo stesso guscio e
   la stessa taglia, quindi la sola cosa che dice a colpo d'occhio QUALE dei due
   si è aperto è questa riga: va letta da lontano, in piedi, con i guanti. */
.qr-overlay__title {
  display: flex; align-items: center; gap: var(--s-3);
  margin: 0; font-size: 1.15rem; font-weight: 700; color: var(--c-text-strong);
  line-height: 1.3;
}
.qr-overlay__icon { font-size: 1.4rem; line-height: 1; }
.qr-overlay__hint { margin: 0; font-size: 0.9rem; color: var(--c-text-muted); line-height: 1.5; }

/* ⭐ LA CASELLA DEL RIQUADRO DEL LETTORE — ADR-0022, e non è un duplicato di
   `#terminal-q`: è il posto dove il fuoco può stare mentre il velo è alzato, e
   dove la lettura SI VEDE ARRIVARE. Grande come la casella in pagina, perché
   fa il lavoro della casella in pagina; monospaziata, perché contiene un codice.

   ⚠️ Il bordo è quello del fuoco anche da ferma: nel riquadro è l'unico campo,
   e un campo che aspetta una lettura deve sembrare acceso. */
.qr-overlay__field {
  width: 100%; min-height: 48px;
  padding: 0 var(--s-3);
  font: inherit; font-size: 1.05rem;
  color: var(--c-text-strong);
  background: var(--c-surface);
  border: 2px solid var(--c-primary); border-radius: var(--r-md);
}
.qr-overlay__field:focus { outline: 2px solid var(--c-primary); outline-offset: 2px; }

/* Le uscite: «Annulla» a sinistra e lo scavalcamento a destra. ⛔ Vanno a capo
   su uno schermo stretto — su un telefono in verticale due tasti affiancati con
   queste etichette finirebbero fuori dal riquadro. */
.qr-overlay__actions {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s-3); margin-top: var(--s-1);
}
/* ⛔ NON è l'azione principale del riquadro: il gesto principale è passare
   l'etichetta, che non ha un tasto. Questo è il ripiego, e deve leggersi come
   tale — bordo e nient'altro, senza riempimento. */
.qr-overlay__swap { margin-inline-start: auto; min-height: 44px; }
/* Il fondo nero non è decorazione: fra il tocco e il primo fotogramma passa
   qualche decimo di secondo, e un rettangolo bianco che diventa nero sembra un
   errore di caricamento. */
.qr-overlay__video {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--r-md); background: #000;
}
.qr-overlay__error { margin: 0; font-size: 0.9rem; color: var(--c-danger); line-height: 1.5; }

/* ---- il pannello di una macchina, DENTRO IL DIALOGO -------------------- */
/* 🔄 QUI C'ERA UN PANNELLO CHE VIVEVA IN PAGINA — ADR-0016, 2026-08-26.

   Cliccando una riga si apre **la modale**, come per ogni altra riga
   dell'applicazione, quindi otto regole sono andate via con gli elementi che
   vestivano: `.terminal-panel` (fondo, raggio, ombra — la scheda adesso è il
   pannello del dialogo), `.terminal-panel[hidden]` (non c'è più un contenitore
   vuoto da nascondere in pagina), la fascia viola `__head` con `__ident`,
   `__code`, `__type` e `__meta` — la testata è del componente, e un frammento
   che se la scrivesse romperebbe la promessa che due dialoghi non possono averne
   due diverse — e `__close`, perché il dialogo ha già la propria chiusura.

   ⛔ SONO STATE TOLTE E NON ANNOTATE ACCANTO: nessuna aveva più un elemento da
   vestire, e una regola senza elemento è una regola che il prossimo copia. Quel
   che identifica la macchina lo dicono ora `.registry__head` e la pastiglia di
   stato, cioè il vocabolario che dialog.css §9 veste già dentro `.modal`. */

/* Nessun `padding`: il corpo del dialogo ha il proprio (dialog.css, «the body»),
   e sommarli darebbe due cornici concentriche attorno alle stesse frasi. */
.terminal-panel__body {
  display: flex; flex-direction: column; gap: var(--s-4);
}
.terminal-panel__fault {
  margin: 0; padding: var(--s-3) var(--s-4);
  background: var(--c-accent-soft);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  line-height: 1.5;
}
.terminal-panel__note { margin: 0; color: var(--c-text-muted); line-height: 1.5; }
.terminal-panel__section {
  margin: 0; font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--c-text-muted);
}
.terminal-panel__hint { margin: 0; font-size: 0.85rem; color: var(--c-text-muted); line-height: 1.5; }

/* La nota per lo storico, sotto i tasti «Avanza a» come nel prototipo. Alta
   quanto un bersaglio da guanto — non 68px come i tasti, perché non è un atto —
   e non passa da `.field` perché non è dentro un `<form>`: il pannello ne
   contiene già uno (la distinta), e un modulo dentro un modulo è HTML che il
   browser ripara spostando gli elementi. */
.terminal-panel__note-field {
  width: 100%; min-height: 52px; padding: var(--s-2) var(--s-4);
  border: 1.5px solid var(--c-border-strong); border-radius: var(--r-md);
  background: var(--c-surface); color: var(--c-text); font: inherit;
}

/* ---- «Avanza a»: i bersagli grandi ------------------------------------- */
.terminal-panel__leaps { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.terminal-panel__leap {
  appearance: none; border: 0; cursor: pointer;
  /* 68px: la misura del terminale, e l'unica cosa di R-018 che questa
     schermata non ha rinegoziato → ripresa.md §14. */
  min-height: 68px; min-width: 12rem; flex: 1 1 12rem;
  padding: 0 var(--s-6);
  border-radius: var(--r-lg);
  color: #fff;
  font-family: inherit; font-size: 1rem; font-weight: 700;
  /* Tre variabili e non tre regole: il colore lo scelgono i modificatori qui
     sotto, e il fondo, l'ombra e l'hover si muovono insieme. Il valore di
     riposo è il viola primario, cioè quello che questi tasti hanno sempre
     avuto — un modificatore mancante non lascia un tasto trasparente. */
  --leap-bg: var(--c-primary);
  --leap-hover: var(--c-primary-strong);
  --leap-shadow: rgba(79, 62, 142, 0.30);
  background: var(--leap-bg);
  box-shadow: 0 3px 8px var(--leap-shadow);
}
/* ⭐ UN COLORE PER STATO DI ARRIVO — richiesta del committente il 2026-08-26:
   tre tasti affiancati devono distinguersi prima di essere letti, e con i guanti
   davanti a uno schermo la differenza di colore arriva prima della parola.

   ⛔ NON SONO COLORI NUOVI: sono gli stessi della mappa `status--*` qui sotto,
   cioè quelli che la pastiglia di quello stato ha in dashboard, in elenco e nel
   pannello. Un tasto «Non riparabile» rosso porta al rosso che la riga avrà dopo
   averlo premuto — se un giorno la mappa cambia, cambiano insieme.
   ⚠️ Il bianco sopra ognuno di questi fondi sta fra 5.4:1 e 14:1, misurato: sono
   i valori scuri della tavolozza, non le loro versioni `-soft`. */
.terminal-panel__leap--inspection {
  --leap-bg: var(--c-info); --leap-hover: #2F4A83;
  --leap-shadow: rgba(58, 91, 158, 0.30);
}
.terminal-panel__leap--awaiting_parts {
  --leap-bg: var(--c-warn-strong); --leap-hover: #6F4C09;
  --leap-shadow: rgba(138, 95, 13, 0.32);
}
.terminal-panel__leap--in_progress {
  --leap-bg: var(--c-primary); --leap-hover: var(--c-primary-strong);
  --leap-shadow: rgba(79, 62, 142, 0.30);
}
.terminal-panel__leap--repaired {
  --leap-bg: var(--c-ok); --leap-hover: var(--c-ok-deep);
  --leap-shadow: rgba(31, 122, 58, 0.32);
}
.terminal-panel__leap--not_repairable {
  --leap-bg: var(--c-danger); --leap-hover: var(--c-danger-deep);
  --leap-shadow: rgba(163, 39, 47, 0.32);
}
.terminal-panel__leap--delivered {
  --leap-bg: var(--c-text-strong); --leap-hover: #1C1C1C;
  --leap-shadow: rgba(43, 43, 43, 0.32);
}
/* ⛔ NESSUNO STATO DI SOLO HOVER (RF-030): su un touch screen esiste solo
   `:active`, e uno stato che il dito non produce è uno stato che il tecnico non
   vede mai. L'hover c'è come cortesia per il mouse, e non dice niente che
   l'`:active` non dica. */
.terminal-panel__leap:hover { background: var(--leap-hover); }
.terminal-panel__leap:active { transform: scale(0.98); }
.terminal-panel__leap:disabled {
  background: var(--c-neutral-soft); color: var(--c-text-muted);
  box-shadow: none; cursor: not-allowed;
}

/* ---- la distinta ------------------------------------------------------- */
.terminal-parts {
  display: flex; flex-direction: column; gap: var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
}
.terminal-parts__title {
  display: flex; align-items: center; gap: var(--s-2);
  margin: 0; font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--c-text-muted);
}
.terminal-parts__count {
  padding: 2px var(--s-2); border-radius: var(--r-pill);
  background: var(--c-primary-soft); color: var(--c-primary-strong);
  font-size: 0.7rem; letter-spacing: 0;
}
.terminal-parts__list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s-2); }
.terminal-parts__row {
  display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--c-border-soft);
  border-radius: var(--r-sm);
  background: var(--c-surface-alt);
}
.terminal-parts__code { font-size: 0.8rem; color: var(--c-text-muted); flex-shrink: 0; }
.terminal-parts__name { font-weight: 600; flex: 1 1 8rem; min-width: 0; }
.terminal-parts__qty { flex-shrink: 0; color: var(--c-text-muted); }
.terminal-parts__drop { flex-shrink: 0; }
.terminal-parts__empty,
.terminal-parts__locked { margin: 0; font-size: 0.85rem; color: var(--c-text-muted); line-height: 1.5; }
/* Il selettore: tendina larga, quantità stretta, tasto che non va a capo. */
.terminal-parts__form {
  display: flex; align-items: flex-end; gap: var(--s-3); flex-wrap: wrap;
}
.terminal-parts__form .field { flex: 1 1 12rem; margin: 0; }
.terminal-parts__form .field:nth-of-type(2) { flex: 0 0 6rem; }
.terminal-parts__form .button { flex-shrink: 0; }

@media (max-width: 640px) {
  /* In verticale i tasti di avanzamento vanno in colonna: due bersagli da 68px
     affiancati su 375px sarebbero due bersagli da 160px, cioè più stretti del
     pollice che devono ricevere. */
  .terminal-panel__leap { flex-basis: 100%; }
}

/* ---------------------------------------------------------- responsive */
/* Mobile-first: the rules above are the narrow viewport. Verified at
   375px and 1440px before any screen is called finished. */

@media (max-width: 640px) {
  .app-main { padding: var(--s-4); }
  .card { padding: var(--s-4); }
  .app-header { padding: 0 var(--s-4); gap: var(--s-3); }
  .toasts { left: var(--s-4); right: var(--s-4); bottom: var(--s-4); }

  /* THE HEADER MUST FIT, and this is not tidying: every item in it refuses to
     wrap (a brand and a person's name are not text to be broken), so anything
     that does not fit does not overflow the header — it widens the PAGE, and
     the whole body then scrolls sideways. Measured at 375px: with the module
     pill and the user's name in place, it did.

     What goes is what is repeated elsewhere: the module name is in the page
     title, and the name is one tap away inside the panel. The avatar stays,
     because «who is signed in» is the question this answers on a shared
     machine. */
  .app-header__module { display: none; }
  .user-chip__name,
  .user-chip__caret { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* ============================================================ dashboard
 * Workshop dashboard: repairs/pages/dashboard.html and its two partials.
 * Layout is mobile-first; the responsive overrides are at the end of the
 * file, after the pre-existing ones, so they win on source order.
 */

/* Visually hidden but announced — for the loading indicator's text. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ------------------------------------------- workflow status → colour map
 * THE map, in one place. A status class sets two custom properties and every
 * component inside consumes them: the pill paints itself, the KPI dot and
 * figure take the same colour. Templates pass `status--{{ order.status|lower }}`
 * and never a colour.
 *
 * DELIVERED and NOT_REPAIRABLE never appear in the dashboard (they are
 * terminal, RF-042); they are defined because the map is the map, and the
 * asset sheet will render the same pills.
 */
.status--accepted       { --status-fg: var(--c-neutral);       --status-bg: var(--c-neutral-soft); }
.status--inspection     { --status-fg: var(--c-info);          --status-bg: var(--c-info-soft); }
.status--awaiting_parts { --status-fg: var(--c-warn-strong);   --status-bg: var(--c-warn-soft); }
.status--in_progress    { --status-fg: var(--c-primary);       --status-bg: var(--c-primary-soft); }
.status--repaired       { --status-fg: var(--c-ok);            --status-bg: var(--c-ok-soft); }
.status--delivered      { --status-fg: var(--c-text-strong);   --status-bg: var(--c-border); }
.status--not_repairable { --status-fg: var(--c-danger);        --status-bg: var(--c-danger-soft); }

/* I cinque stati del NOLEGGIO, dal 2026-08-14. Dominio diverso, stessa mappa e
 * — questo è il punto — **gli stessi token**: il prototipo v3 dipinge i suoi KPI
 * con #3F8A46, #4F3E8E e #B14444 scritti a mano, che sono tre verdi/viola/rossi
 * vicini ma non uguali a quelli dell'applicazione. Tre colori quasi giusti su una
 * schermata sono il modo in cui un prodotto smette di sembrare uno.
 *
 * Le classi si compongono dal valore di `RentalState` abbassato
 * (`status--{{ row.state|lower }}`), come per le riparazioni: una convenzione
 * sola per due domini.
 *
 * `late` prende il rosso di `not_repairable` e non un rosso proprio: sono due
 * modi di dire «questa riga chiede una telefonata». */
.status--available      { --status-fg: var(--c-ok);            --status-bg: var(--c-ok-soft); }
.status--rented         { --status-fg: var(--c-primary);        --status-bg: var(--c-primary-soft); }
.status--late           { --status-fg: var(--c-danger);         --status-bg: var(--c-danger-soft); }
.status--maintenance    { --status-fg: var(--c-warn-strong);    --status-bg: var(--c-warn-soft); }
.status--retired        { --status-fg: var(--c-text-strong);    --status-bg: var(--c-border); }

.status-pill {
  display: inline-block; white-space: nowrap;
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-sm);
  font-size: 0.72rem; font-weight: 600;
  background: var(--status-bg); color: var(--status-fg);
}

/* ------------------------------------------------------------- page head */

/* A row, not a stack: on the registries the page-level action sits at the right
   end of the head — «+ Nuovo operatore» belongs to the screen, not to the table.
   Every page wraps its title and crumbs in .page-head__text, so h1 and crumbs do
   not become two flex items side by side. */
.page-head {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s-4);
}
.page-head__text { margin-right: auto; min-width: 0; }
.page-head h1 { margin-bottom: var(--s-1); }
.page-head__crumbs { margin: 0; font-size: 0.78rem; color: var(--c-text-muted); }

@media (max-width: 640px) {
  /* The action takes a line of its own, which is also a bigger target. */
  .page-head > .button { flex: 1 1 100%; }
}

/* ------------------------------------------------------- status counters
 * I sei riquadri di stato — RF-012 — che dal 2026-08-25 sono anche i filtri
 * (RF-011). Ridisegnati lo stesso giorno, su richiesta del committente.
 *
 * ⚠ E LA PRIMA RAGIONE DEL RIDISEGNO NON È L'ESTETICA: LE SEI CIFRE NON STAVANO
 * SU UNA RIGA. Il riquadro era `justify-content: center`, quindi il contenuto si
 * centrava sull'altezza della PROPRIA card; «Riparato» porta un `hint` che manda
 * l'etichetta a capo, e il suo numero finiva 7px più in basso degli altri cinque
 * — misurato, non stimato. Una riga di cifre che non si allineano si legge una
 * per una invece che d'un colpo, che è l'opposto del lavoro di un contatore.
 *
 * `space-between` è la correzione, e vale finché le card sono alte uguali: la
 * griglia le stira (`align-items: stretch` è il suo valore iniziale), quindi
 * l'etichetta sta in cima, la cifra in fondo, e il fondo è lo stesso per tutte
 * e sei qualunque cosa faccia l'etichetta. ⛔ Ne segue un vincolo: qualunque cosa
 * si aggiunga SOTTO la cifra va aggiunta a tutte e sei, o l'allineamento torna a
 * rompersi in una card sola — che è il modo in cui non si nota.
 *
 * ── LE TRE COSE CHE DANNO IL COLORE, E NON NE SCRIVONO NESSUNO ──────────────
 *
 * La mappa stato→colore resta l'unica (§status → colour map): la card riceve
 * `status--<value>` dal template e legge `--status-fg` / `--status-bg`. Qui
 * dentro non c'è un colore di stato scritto, ed è ciò che permette di cambiare
 * la mappa in un punto solo.
 *
 *   1. la velatura — `--status-bg` che sfuma nel bianco da un angolo. È il
 *      tocco che fa sembrare la card «di quello stato» senza dipingerla: i
 *      cinque `*-soft` della palette sono già tarati come fondo di pastiglia,
 *      quindi a bassa copertura sono un soffio;
 *   2. il pallino, con l'alone dello stesso `*-soft` — un LED, non un bollo;
 *   3. la cifra, che porta `--status-fg` come ha sempre fatto.
 *
 * ── LO STATO ACCESO SI VEDE DA LONTANO, E PRIMA NO ─────────────────────────
 *
 * Prima la card attiva cambiava soltanto il colore di un bordo da 2px: in una
 * riga di sei, guardando lo schermo, non si distingueva. Adesso cambiano tre
 * cose insieme — la velatura arriva a coprire tutta la card, il bordo prende il
 * colore dello stato, l'etichetta smette di essere grigia — e nessuna delle tre
 * sposta un pixel: il bordo trasparente c'era già, la velatura è un gradiente e
 * il colore del testo non ha geometria. È la ragione per cui `--kpi-wash` è una
 * custom property e non tre gradienti scritti tre volte.
 */

.kpi {
  display: grid;
  /* 9.5rem invece di 9: a 1440px con la navigazione agganciata le sei card
     stanno comunque su una riga, e sotto quella misura l'etichetta più lunga
     («In attesa ricambi») andava a capo su tutte e sei invece che su nessuna. */
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: var(--s-3);
}

.kpi__tile {
  /* Quanta parte della card copre la velatura dello stato. Un solo numero da
     cambiare per i tre stati (riposo, hover, accesa), invece di tre gradienti
     che devono restare d'accordo. */
  --kpi-wash: 68%;

  display: flex; flex-direction: column; justify-content: space-between;
  /* Il `gap` non decide niente quando la card è alta più del suo contenuto —
     `space-between` distribuisce — ma è il minimo garantito nella card più
     stretta, dove l'etichetta va a capo due volte. */
  gap: var(--s-3);
  min-height: 6rem; padding: var(--s-3) var(--s-4) var(--s-4);
  border-radius: var(--r-lg);
  /* Il riferimento del binario della quota, e la forbice che ne arrotonda le
     due estremità sugli angoli della card. ⚠️ `overflow: hidden` su un riquadro
     che si solleva al passaggio del mouse non è un problema — il sollevamento è
     un `transform`, che non è contenuto da niente — ma su un riquadro con un
     menu a tendina dentro lo sarebbe: qui dentro non ce n'è, e non ce ne può
     essere, perché una card è un collegamento intero. */
  position: relative; overflow: hidden;
  /* ⚠ `background-color` + `background-image` E NON L'ABBREVIAZIONE `background`:
     l'abbreviazione azzera l'altra metà, e le due qui sotto sono due strati —
     il bianco della card e la velatura dello stato sopra di esso. È la stessa
     trappola dell'abbreviazione `border` che si portava dietro il colore
     (ripresa.md §9-vicies-ter), in un'altra proprietà. */
  background-color: var(--c-surface);
  background-image: linear-gradient(
    152deg,
    var(--status-bg, transparent) 0%,
    transparent var(--kpi-wash)
  );
  box-shadow: var(--shadow-1);
}

/* L'etichetta parla con la voce delle micro-intestazioni di QUESTA schermata —
   la testata della tabella qui sotto e il titolo della navigazione: 0.66rem,
   maiuscoletto spaziato, grigio. Non con quella del dialogo (mono, oro), che è
   un altro strato e un'altra lingua: due voci per la stessa cosa su una pagina
   sola sono il modo in cui un prodotto smette di sembrare uno. */
.kpi__label {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  line-height: 1.35; color: var(--c-text-muted);
  transition: color var(--t-fast);
}

/* «attende il ritiro» non è un'etichetta: è una precisazione, e in maiuscoletto
   spaziato urlerebbe quanto il nome dello stato. Torna minuscola e leggermente
   più grande — le due misure stanno su una riga sola perché il contenitore le
   allinea al centro, non alla base. */
.kpi__hint {
  font-size: 0.7rem; font-weight: 500;
  letter-spacing: normal; text-transform: none;
}

/* Il pallino: 8px con un alone di 3px nel colore tenue dello stesso stato.
   L'alone è ciò che lo fa leggere come una spia accesa invece che come un
   bollo — e non costa un elemento in più, è un'ombra. */
.kpi__dot {
  width: 8px; height: 8px; flex-shrink: 0;
  border-radius: var(--r-pill);
  background: var(--status-fg, var(--c-neutral));
  box-shadow: 0 0 0 3px var(--status-bg, transparent);
  transition: box-shadow var(--t-fast);
}

/* La cifra. `tabular-nums` non è un vezzo: senza, in Poppins il «1» è più
   stretto delle altre cifre, quindi «11» e «27» hanno larghezze diverse e la
   colonna di numeri si muove a ogni ricarica dei contatori. `-0.02em` è la
   stessa correzione che l'`h1` di questa pagina fa a 1.375rem: una cifra grande
   con la spaziatura del testo sembra sparpagliata. */
.kpi__value {
  font-size: 2rem; font-weight: 700; line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--status-fg, var(--c-text-strong));
}

/* ── LA QUOTA SUL TOTALE: 3px SUL BORDO INFERIORE ───────────────────────────
 *
 * Rima con il manometro del dialogo (dialog.css §the gauge), che è il modo in
 * cui questo prodotto disegna già «una parte di un tutto»: stessa altezza,
 * estremo sinistro quadrato ed estremo destro tondo — è quello che fa leggere
 * una barra come qualcosa che CRESCE da una parete invece che come un segmento
 * appoggiato lì.
 *
 * ⛔ IN POSIZIONE ASSOLUTA, E NON PER PIGRIZIA: il vincolo dichiarato in cima a
 * questa sezione dice che qualunque cosa sotto la cifra romperebbe l'allineamento
 * delle sei cifre nella card che non l'ha — e la card totale non ha il binario.
 * Fuori dal flusso non occupa niente, quindi non può spostare nulla.
 *
 * ⚠️ LA TRACCIA È UN SUSSURRO, E LA FRAZIONE SI LEGGE COMUNQUE — guardato a
 * schermo, non dedotto. `--c-border` sul bianco è a un soffio dall'invisibile, e
 * l'alternativa era `--c-border-strong` — il token fatto per «un segno che si
 * deve vedere» — cioè sei righe grigie in fila sotto la riga delle card. Non
 * serve: le sei card sono larghe UGUALI (`1fr`), quindi le sei lunghezze si
 * confrontano fra loro, che è la domanda vera («è lo stato che ne ha di più?»);
 * e dove sta il 100% lo dice la card totale, che è il denominatore.
 * Il pieno è `--status-fg`, cioè lo stesso colore della cifra che misura — un
 * secondo colore qui vorrebbe dire un secondo significato.
 */
.kpi__share {
  position: absolute; inset: auto 0 0;
  height: 3px;
  background: var(--c-border);
}
.kpi__share-fill {
  display: block; height: 100%;
  border-radius: 0 3px 3px 0;
  background: var(--status-fg, var(--c-neutral));
}

/* ── LA CARD TOTALE: L'UNICA CIFRA CHE SI LEGGE DALL'ALTRO LATO DELLA STANZA ──
 *
 * Non ha uno stato, quindi non ha né `--status-fg` né `--status-bg`: il suo
 * colore è il viola del marchio, e i due token sono già una coppia — `--c-primary`
 * e il suo premuto `--c-primary-strong` — quindi la diagonale è fatta di
 * vocabolario esistente e non di due esadecimali nuovi. Il velo bianco
 * dall'angolo alto è l'unico valore scritto a mano, ed è la stessa forma degli
 * `rgba()` con cui questo foglio dichiara le ombre.
 *
 * L'ombra è quella dei pulsanti primari, tinta di viola: sotto un riquadro
 * viola un'ombra grigia sembra un errore di resa (nota di `--shadow-cta-primary`).
 */
.kpi__tile--total {
  background-color: var(--c-primary);
  background-image:
    radial-gradient(120% 100% at 100% 0%, rgb(255 255 255 / 0.18), transparent 62%),
    linear-gradient(150deg, var(--c-primary) 0%, var(--c-primary-strong) 100%);
  box-shadow: var(--shadow-cta-primary);
}
/* 0.82 e non il bianco pieno: l'etichetta deve stare SOTTO la cifra nella scala
   di attenzione, e sul viola resta a 5.4:1 — sopra il 4.5:1 richiesto. */
.kpi__tile--total .kpi__label { color: rgb(255 255 255 / 0.82); }
.kpi__tile--total .kpi__value { color: #fff; font-size: 2.25rem; }

/* ── LE CARD SONO FILTRI — 2026-08-25 ────────────────────────────────────────
 *
 * Ognuna e' un `<a>` verso la dashboard con `?status=`, quindi eredita colore e
 * sottolineatura dei collegamenti: le due righe qui sotto le tolgono, perche'
 * cio' che deve leggersi come premibile e' la CARD, non le sue parole.
 *
 * ⚠ `a.kpi__tile` E NON `.kpi__tile`: il selettore di elemento tiene le regole
 * attaccate al fatto che la card sia diventata un collegamento. Il giorno che
 * una tornasse un `<div>` — un contatore che non filtra — smetterebbe da sola di
 * prendersi il cursore a manina, invece di continuare a prometterlo. */
a.kpi__tile {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  /* Il bordo c'e' SEMPRE, trasparente: e' quello che la card accesa colora. Un
     bordo aggiunto solo da `--on` sposterebbe il contenuto di 2px al clic. */
  border: 2px solid transparent;
  transition: box-shadow var(--t-fast), border-color var(--t-fast),
              transform var(--t-fast);
}

/* Si solleva di 2px, che è il modo più economico di dire «premibile» a un
   riquadro che non ha l'aria di un tasto. ⚠️ Sotto `prefers-reduced-motion`
   la regola globale di riga 636 azzera la transizione: il sollevamento resta,
   istantaneo, che è ciò che quella preferenza chiede — nessun movimento, non
   nessuna risposta. */
a.kpi__tile:hover {
  box-shadow: var(--shadow-2);
  transform: translateY(-2px);
}
a.kpi__tile--total:hover { box-shadow: var(--shadow-2), var(--shadow-cta-primary); }

/* La card attiva: il bordo prende il colore dello stato che rappresenta —
   `--status-fg` lo dichiara gia' la classe `status--*` sulla stessa card, quindi
   qui non c'e' nessun colore scritto. La totale non ha uno stato e usa il bianco
   sul viola, che e' l'unico contrasto che quella card ha a disposizione. */
/* ⚠ `a.kpi__tile--on` E NON `.kpi__tile--on`, ED E' STATO MISURATO NEL BROWSER.
   La regola sopra dichiara `border` per intero — colore compreso — su un
   selettore che vale (0,1,1); una classe sola vale (0,1,0) e PERDE, quindi la
   card accesa restava col bordo trasparente. Non e' una gara di specificita'
   vinta per caso: e' la stessa forma che l'abbreviazione `border` ha creato
   dandosi anche il colore, ed e' il motivo per cui il test di browser di questa
   riga misura due card e non una. */
a.kpi__tile--on {
  border-color: var(--status-fg, var(--c-accent));
  /* La velatura arriva in fondo: `160%` e non `100%` perché il secondo stop di
     un gradiente è dove il colore è ESAURITO, quindi a 100% la card sfumerebbe
     comunque nel bianco nell'angolo opposto. Oltre il 100% la sfumatura viene
     tagliata dal riquadro e la tinta resta piena fin sul bordo. */
  --kpi-wash: 160%;
}
/* L'etichetta della card accesa esce dal grigio e prende il colore del proprio
   stato: è il terzo segnale, e serve a chi guarda la riga di sbieco.

   ⚠️ E IL CONTRASTO REGGE PERCHÉ LE CINQUE COPPIE SONO GIÀ QUELLE MISURATE: un
   `--status-fg` sul proprio `--status-bg` è esattamente la coppia che il blocco
   §alert dichiara riga per riga (danger 6.2:1 · info 5.7:1 · ok 4.7:1 · warn
   5.0:1), ed è la ragione per cui `awaiting_parts` legge `--c-warn-strong` e non
   `--c-warn` nella mappa. Nessuna coppia nuova, nessun numero da rimisurare. */
a.kpi__tile--on .kpi__label { color: var(--status-fg, var(--c-text-strong)); }
/* L'alone del pallino si allarga: la spia è accesa. */
a.kpi__tile--on .kpi__dot { box-shadow: 0 0 0 4px var(--status-bg, transparent); }

a.kpi__tile--total.kpi__tile--on { border-color: #fff; }
/* ⛔ La totale non ha `--status-bg`, quindi la velatura non la riguarda: sul
   viola l'unico segnale disponibile è il bordo bianco, e l'etichetta resta
   quella che è — portarla al bianco pieno la metterebbe alla pari della cifra. */
.kpi__tile--total.kpi__tile--on .kpi__label { color: rgb(255 255 255 / 0.92); }

/* --------------------------------------------------------- orders panel */

/* A card whose children carry their own padding, so the table can run edge
   to edge. */
.card--flush { padding: 0; overflow: hidden; }

.orders-panel__head {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4) var(--s-6) 0;
}
.orders-panel__head h2 { margin: 0; }

/* Mandatory feedback on every network action: in a workshop with a weak
   connection, an action without a visible response is a broken interface. */
.spinner {
  width: 18px; height: 18px; flex-shrink: 0;
  border: 2px solid var(--c-border); border-top-color: var(--c-primary);
  border-radius: var(--r-pill);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.filters {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--s-3);
  padding: var(--s-4) var(--s-6);
}
.filters__field { display: flex; flex-direction: column; gap: var(--s-1); flex: 1 1 12rem; min-width: 0; }
.filters__label { font-size: 0.8rem; font-weight: 600; color: var(--c-text-strong); }
.filters__actions { display: flex; align-items: center; gap: var(--s-4); flex: 0 0 auto; }
.filters__reset {
  display: inline-flex; align-items: center;
  min-height: var(--touch-min); font-size: 0.85rem; font-weight: 600;
}

/* ------------------------------------------------------------ the table */

/* The table scrolls inside its own container: the page body never scrolls
   horizontally. tabindex on the container makes it scrollable from the
   keyboard too. */
/* No border-top here: the sticky-looking rule above the head is the head's own
   border, and two of them stack into a double line.
 *
 * `contain: paint` IS LOAD-BEARING, and it was measured, not added for luck.
 * `overflow-x: auto` alone did NOT stop this table from widening the whole
 * document: at a 485px viewport the page scrolled sideways by 123px, and — with
 * the stylesheet as it was before the design pass — by 48px, so the defect was
 * already there and Poppins only made it wider. A wide table inside a scroll
 * container still propagated its minimum width up through the container.
 *
 * Verified in Chrome by setting `document.scrollingElement.scrollLeft = 9999`
 * and reading it back: 123 before, 0 after. That is the check to repeat, because
 * a screenshot does NOT show it — headless Chrome renders at its own window
 * width and crops, so the page looks fine at «375px» while a real phone scrolls.
 *
 * `table-layout: fixed` also fixed it and was rejected: it squeezes eight
 * columns into 375px, which is the opposite of what the scroll container is for.
 */
.table-scroll { overflow-x: auto; contain: paint; }

.data-table { width: 100%; min-width: 54rem; border-collapse: collapse; font-size: 0.8125rem; }
.data-table thead th {
  padding: var(--s-2) var(--s-3);
  background: var(--c-surface-alt);
  border-top: 1px solid var(--c-border); border-bottom: 1px solid var(--c-border);
  text-align: left; white-space: nowrap;
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--c-text-muted);
}
.data-table td,
.data-table tbody th {
  padding: var(--s-3); border-bottom: 1px solid var(--c-border-soft);
  text-align: left; vertical-align: middle; font-weight: 400;
}
.data-table tbody tr:last-child td,
.data-table tbody tr:last-child th { border-bottom: 0; }

/* Feedback under the cursor. Not decoration on a wide table: it is what keeps
   the eye on one row while it travels from the code on the left to the actions
   on the right. Transition omitted deliberately — a fading row lags behind the
   pointer and reads as sluggishness. */
.data-table tbody tr:hover { background: var(--c-row-hover); }
.data-table th:first-child, .data-table td:first-child { padding-left: var(--s-6); }
.data-table th:last-child,  .data-table td:last-child  { padding-right: var(--s-6); }
.data-table .col-num { text-align: right; white-space: nowrap; }
/* Una cella che non va a capo pur non essendo un numero — 2026-08-24, per la
   colonna «Atteso da» del magazzino. Un codice d'ordine spezzato su due righe
   («RT-2026-» sopra, «0035» sotto) è un codice che non si legge al telefono. */
.data-table .col-nowrap { white-space: nowrap; }

.cell-code { white-space: nowrap; }
.cell-code__value { display: block; font-weight: 600; color: var(--c-text-strong); }
.cell-code__priority { display: block; font-size: 0.72rem; color: var(--c-text-muted); }
.cell-type { min-width: 12rem; }

.operator { display: inline-flex; align-items: center; gap: var(--s-2); }
/* The per-operator colour arrives as an inline background-color from the
   record. It is optional (Operator.color is blank=True), so the default
   lives here and not as a hex inside the template. */
.operator__avatar {
  width: 28px; height: 28px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  background: var(--c-primary);
  font-size: 0.72rem; font-weight: 700; color: #fff;
}
/* An order in "Accettato" has no technician yet: the empty case is normal,
   so it is drawn, not left blank. */
.operator__avatar--empty { background: transparent; border: 1.5px dashed var(--c-neutral); }
.operator--unassigned .operator__name { color: var(--c-text-muted); font-style: italic; }

.orders-table > .empty-state { margin: var(--s-4) var(--s-6) var(--s-6); }

.orders-table__foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-6);
  border-top: 1px solid var(--c-border);
}
.orders-table__count { margin: 0; font-size: 0.78rem; color: var(--c-text-muted); }
/* La frase che dice cosa fa il clic su una riga. Piu' chiara del conteggio non
   deve essere: e' un'istruzione, non un dato. */
.orders-table__hint { color: var(--c-text-muted); }

/* ── LA RIGA CLICCABILE — 2026-08-25, RF-013 ────────────────────────────────
 *
 * `.data-table tbody tr:hover` dipinge gia' lo sfondo di ogni riga: qui si
 * aggiunge solo il cursore, cioe' la promessa che il clic faccia qualcosa. Su
 * una tabella le cui righe NON aprono niente quella promessa sarebbe falsa, ed
 * e' la ragione per cui la classe e' sulla riga e non su `.data-table`. */
.row--clickable { cursor: pointer; }

/* Il codice dell'ordine e' un `<button>` perche' una riga non si preme col
   tastierino: il clic risale al `<tr>`, che e' dove HTMX ascolta. Queste sei
   proprieta' sono cio' che toglie a un tasto l'aspetto di tasto — resta un
   comando per la tastiera e per lo screen reader, e a schermo e' il codice. */
.row-open {
  padding: 0; border: 0; background: none;
  font: inherit; text-align: left; cursor: pointer;
  color: var(--c-text-strong);
}

/* Un tasto che deve leggersi come un collegamento: apre un pannello o carica un
   frammento, quindi non e' un `<a>`, ma nella frase in cui sta e' una parola
   premibile e non un rettangolo. */
.link-button {
  padding: 0; border: 0; background: none;
  font: inherit; font-weight: 600; cursor: pointer;
  color: var(--c-primary); text-decoration: underline;
}
.link-button:hover { color: var(--c-primary-strong); }

/* ── L'ARCHIVIO, dentro l'elenco della dashboard — RF-042 ───────────────────
 *
 * Non e' una `.card`: sta gia' dentro quella dell'elenco. Il filo sopra e' cio'
 * che dice «questi non sono in officina» senza scriverlo una seconda volta. */
.archive { border-top: 1px solid var(--c-border); }
.archive__head {
  margin: 0; padding: var(--s-4) var(--s-6) var(--s-3);
  font-size: 0.9rem; color: var(--c-text-strong);
}
/* Il taglio dichiarato: e' una nota, non un avviso — chi cerca una matricola
   intera non la incontra mai. */
.archive__more {
  margin: 0; padding: 0 var(--s-6) var(--s-4);
  font-size: 0.8rem; color: var(--c-text-muted);
}

/* L'elenco dei passaggi precedenti dentro l'avviso dell'accettazione: tre dati
   su una riga, senza i pallini di una lista — sono righe di dati, non punti di
   un discorso. */
.lookup-list { list-style: none; margin: var(--s-2) 0 0; padding: 0; }
.lookup-list li {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  padding: var(--s-1) 0;
}

/* ── L'ANTEPRIMA DEL QR nella schermata d'esito ─────────────────────────────
 *
 * Nessuna misura sull'SVG: `segno` scrive larghezza e altezza sull'elemento
 * (`svg_inline`), e la scala arriva dalla view. Una regola qui sarebbe una
 * seconda dichiarazione della stessa misura, e vincerebbe in silenzio. */
.qr-preview {
  display: flex; align-items: center; gap: var(--s-4);
  margin: 0 0 var(--s-4);
}
.qr-preview__image { display: inline-flex; }
.qr-preview__code { font-size: 1.1rem; font-weight: 700; color: var(--c-text-strong); }

.pager { display: flex; align-items: center; gap: var(--s-2); margin-left: auto; }
.pager__link {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--touch-min); padding: 0 var(--s-3);
  border-radius: var(--r-md);
  font-size: 0.85rem; font-weight: 600; text-decoration: none;
  color: var(--c-primary);
}
.pager__link:hover { background: var(--c-primary-soft); }
.pager__link--off { color: var(--c-text-muted); opacity: 0.55; }
.pager__status { font-size: 0.85rem; color: var(--c-text-muted); }

/* ------------------------------------------------- dashboard, narrow view
 * Verified at 375px. Two columns of counters, stacked filters, and the two
 * lowest-value columns dropped so the table scroll stays short — the data
 * that identifies the asset (code, type, serial) never hides.
 */
@media (max-width: 640px) {
  .card--flush { padding: 0; }

  .kpi { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Due colonne invece di sei: la card è larga il doppio e alta uguale, quindi
     la cifra può restare grande — scende solo quanto basta perché «Asset in
     officina» non vada a capo tre volte accanto a un 2rem. */
  .kpi__value { font-size: 1.75rem; }
  .kpi__tile--total .kpi__value { font-size: 2rem; }

  .orders-panel__head { padding: var(--s-4) var(--s-4) 0; }
  .filters { padding: var(--s-4); }
  .filters__field { flex: 1 1 100%; }
  .filters__actions { flex: 1 1 100%; justify-content: space-between; }

  .col-secondary { display: none; }
  .data-table { min-width: 34rem; font-size: 0.85rem; }
  .data-table th:first-child, .data-table td:first-child { padding-left: var(--s-4); }
  .data-table th:last-child,  .data-table td:last-child  { padding-right: var(--s-4); }

  .orders-table > .empty-state { margin: var(--s-4); }
  .orders-table__foot { padding: var(--s-3) var(--s-4); }
  .pager { margin-left: 0; width: 100%; justify-content: space-between; }
}

/* ============================================================ components
 * components/_alert.html and components/_modal.html — the two shared pieces
 * the CRUD-form-in-a-modal pattern is built from. No domain here: neither of
 * them has heard of a repair order.
 */

/* ---------------------------------------------------------------- alert
 * Persistent, in-flow notice (level 3 of the error handling in
 * docs/04-frontend/architettura-ui.md). Same custom-property trick as
 * .status--*: the variant sets two properties and the parts inside consume
 * them, so a template passes a kind and never a colour.
 *
 * The base class carries safe defaults on purpose. An unknown or misspelled
 * kind then degrades to neutral instead of leaving --alert-fg empty, which is
 * how you get text painted on a background of its own colour.
 *
 * Contrast of the message on its background, all above the required 4.5:1:
 * danger 6.2:1 · info 5.7:1 · ok 4.7:1 · warn 5.0:1 (which is why warn uses
 * --c-warn-strong and not --c-warn, at 4.0:1).
 */
.alert {
  --alert-fg: var(--c-text-strong);
  --alert-bg: var(--c-neutral-soft);

  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  margin: 0 0 var(--s-4);
  background: var(--alert-bg);
  color: var(--alert-fg);
  border-left: 4px solid var(--alert-fg);
  border-radius: var(--r-md);
}

.alert--danger { --alert-fg: var(--c-danger);      --alert-bg: var(--c-danger-soft); }
.alert--warn   { --alert-fg: var(--c-warn-strong); --alert-bg: var(--c-warn-soft); }
.alert--ok     { --alert-fg: var(--c-ok);          --alert-bg: var(--c-ok-soft); }
.alert--info   { --alert-fg: var(--c-info);        --alert-bg: var(--c-info-soft); }

/* The glyph tells the variants apart without relying on colour. */
.alert__icon {
  flex-shrink: 0;
  width: 1.5rem; height: 1.5rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  background: var(--alert-fg); color: var(--c-surface);
  font-size: 0.8rem; font-weight: 700; line-height: 1;
}

.alert__content { flex: 1; min-width: 0; }
.alert__title   { margin: 0; font-weight: 700; }
.alert__message { margin: 0; }

.alert__close {
  flex-shrink: 0;
  background: none; border: none; cursor: pointer;
  font-size: 1.25rem; line-height: 1; color: inherit;
  min-width: var(--touch-min); min-height: var(--touch-min);
}

/* ---------------------------------------------------------------- modal
 * ⛔ SI E' TRASFERITO — 2026-08-07 → `static/css/dialog.css`.
 *
 * Il dialogo e' il primo componente a passare al linguaggio iOS di ADR-0011, e
 * non poteva essere vestito da qui: questo file e' interamente NON stratificato,
 * e in CSS una regola non stratificata batte qualunque `@layer` a prescindere
 * dalla specificita'. Finche' `.modal` stava scritto qui, qualsiasi cosa lo
 * strato nuovo dicesse veniva persa.
 *
 * `dialog.css` e' anch'esso non stratificato, viene caricato dopo questo file
 * (vedi base.html) ed e' tutto sotto `.modal`: vince dove deve, e in nessun
 * altro punto dell'applicazione.
 *
 * Di quel blocco resta qui una cosa sola, qui sotto: il trattamento delle barre
 * di scorrimento, perche' era condiviso con `.table-scroll` e con `html`, che
 * sono ancora di questo foglio.
 */

/* --------------------------------------------------------- scroll surfaces
 * ⚠ LA BARRA DI SCORRIMENTO E' PARTE DEL DISEGNO SU WINDOWS, ed era sfuggita
 * una volta: Chromium headless le disegna a sovrapposizione — invisibili finche'
 * non si scorre — mentre Windows ne disegna una classica, 17px, grigia, con le
 * frecce. Misurato in una finestra vera, non su uno screenshot → ripresa.md §9.
 *
 * `dialog.css` ripete queste misure invece di importarle. Le due devono
 * accordarsi su come si vede una barra — 8px, sottile, traccia trasparente — e
 * lo fanno, ma non condividono piu' un selettore: una e' v3, l'altra e' iOS.
 */
html,
.table-scroll {
  scrollbar-width: thin;
  scrollbar-color: var(--c-border-strong) transparent;
}
/* Safari before 18.2 has no `scrollbar-width`. Same measurements, so the two
   engines agree; declared after, so a browser that understands both is not
   affected by which one it applies. */
.table-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.table-scroll::-webkit-scrollbar-track { background: transparent; }
.table-scroll::-webkit-scrollbar-thumb {
  background: var(--c-border-strong);
  border-radius: var(--r-pill);
}
.table-scroll::-webkit-scrollbar-thumb:hover { background: var(--c-text-muted); }


/* ================================================================ form panel
 * The registry form — clienti, tipologie, operatori, ricambi. It is THE form of
 * the four registries, and since 2026-08-07 it lives inside the dialog
 * (`.modal--wide`), where it was before 2026-08-06 as well. The class names kept
 * the `form-panel` prefix through both moves: they describe the BLOCKS, which
 * did not change, and renaming them would have touched four templates to say
 * the same thing.
 *
 * What the dialog gives back, beyond shape: the inert background. While the
 * form was in line, suspending somebody from a row could throw away edits open
 * on another person, and the view had to guard against it by hand.
 *
 * The blocks are real <fieldset>s with a <legend>: a form of eight fields
 * navigated by a screen reader is a flat list of eight controls unless the
 * grouping is in the markup. And the grouping is not decoration here — it is
 * what keeps the badge away from the password (ADR-0003: the badge is a
 * signature, not a credential).
 */

/* The blocks inside the form need their own spacing, and a <fieldset> with its
   margin reset has none. Without this the legend of one block sits against the
   help text of the block above.

   ── AND IT IS THE QUERY CONTAINER OF THE FORM ───────────────────────────────

   Declared here so `.form-panel__grid` below can ask how wide THE PANEL is
   instead of how wide the window is. See the grid for why that difference is
   the whole point.

   On the form and not on `.modal__body`, for two reasons: it is the only
   ancestor a grid is guaranteed to have wherever the fragment is rendered — the
   profile page renders `.form-panel__form` outside any dialog — and it keeps
   the mechanism in the same file as the grid it serves.

   `inline-size` and never `size`: the block axis must stay free. The form grows
   with its content and `.modal__body` scrolls it; a form whose height was
   contained would be a form measured as if it were empty. */
.form-panel__form {
  display: flex; flex-direction: column; gap: var(--s-6);
  container: form-panel / inline-size;
}

/* A fieldset carries a browser border and padding by default, and looks nothing
   like the rest of the card. Reset, then rebuild. */
.form-panel__group {
  min-width: 0;            /* a fieldset does not shrink below its content without this */
  margin: 0; padding: 0; border: 0;
  display: flex; flex-direction: column; gap: var(--s-3);
}
.form-panel__legend {
  padding: 0; margin: 0 0 var(--s-1);
  font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--c-text-muted);
}

/* ── THE COLUMNS FOLLOW THE PANEL, NOT THE WINDOW ────────────────────────────
 *
 * ⚠ QUESTE TRE FORME LE DECIDEVANO DUE `@media`, ED È IL DIFETTO SEGNALATO
 * L'11-08-2026.
 *
 * The dialog can be RESIZED (dialog.css §resize), and a viewport breakpoint
 * cannot see a panel the reader has just narrowed. Dragging the 52rem panel down
 * to ~365px on a 1920px screen left the three columns exactly where the media
 * query had put them: «Partita IVA» wrapped onto two lines, «Città» got 85px,
 * and the row of inputs stopped lining up — `align-items: start` aligns each
 * cell to the top of ITS OWN label, so one wrapped label drops one input by a
 * line. Nothing was broken in the grid: it was being asked the wrong question.
 *
 * So the question changed. The form declares itself a query container (above)
 * and the three shapes below ask IT. Narrow the panel and the columns fold,
 * widen it and they come back; the default 52rem panel still renders in three
 * columns, so the register form looks exactly as it did.
 *
 * The window has nothing to do with it any more, and it does not need to: on a
 * phone the panel IS the width of the sheet, so the container gives the same
 * answer the media query used to give.
 */
.form-panel__grid {
  display: grid;
  /* One column is the base, mobile-first: it is the shape that reads at any
     width, and it is what a form rendered with no container to ask would get. */
  grid-template-columns: 1fr;
  gap: var(--s-4);
  align-items: start;
}

/* Load-bearing at the panel's smallest measure, not tidiness: a grid item's
   automatic minimum size is its CONTENT's, so a field whose label does not fit
   refuses to shrink and pushes the row out of the panel — a form that scrolls
   sideways — instead of wrapping inside it. */
.form-panel__grid > * { min-width: 0; }

/* Two columns before three: below this each of three fields is too narrow to
   read its own label on one line, which is the state the defect was reported
   in. */
@container form-panel (min-width: 24rem) {
  .form-panel__grid { grid-template-columns: 1fr 1fr; }
}

/* v3's proportions: the first column is the wide one, and it is where the field
   that carries a company name or a description goes. */
@container form-panel (min-width: 40rem) {
  .form-panel__grid { grid-template-columns: 1.6fr 1fr 1fr; }
}
/* The field is already spaced by the grid: its own bottom margin would add a
   second, uneven gap between rows.

   ⚠ THE SELECTOR WAS `.form-panel .field` AND MATCHED NOTHING — fixed 2026-08-07.
   No template has ever rendered `class="form-panel"`: the classes are all
   `form-panel__*`, so this reset was dead from the day it was written and every
   field in the dialog kept its 1rem bottom margin. It was invisible in the grid,
   where it only widened the gap between rows — and visible in the badge block,
   whose row aligns the «Genera QR» button to the BOTTOM of the field: the button
   sat 16px below the input it belongs to. A dead selector does not fail, it
   just stops doing its job quietly. */
.form-panel__form .field { margin-bottom: 0; }

/* A field whose value is SHORT — a hex colour, a badge code — keeps its control
   short instead of being moved to a narrow column. Placing it elsewhere in the
   grid was the first attempt and looked broken: the legend stayed on the left,
   the field sat in the middle, and two thirds of the row were a hole. The field
   stays where reading order puts it; only the input stops pretending it is
   waiting for a sentence.
   ⚠ Nessun template la usa oggi: è la forma che aspetta i registri che verranno
   (una matricola, un codice ricambio). Se al primo di quelli non serve, va via
   come è andata via `--wide` qui sotto. */
.form-panel__cell--narrow input { max-width: 14rem; }

/* ⛔ `.form-panel__cell--wide` È SPARITA — 2026-08-07, e vale la pena dire
   perché invece di lasciarla lì «che magari serve».

   Era una cella larga tutta la riga (`grid-column: 1 / -1`) e l'aveva chiesta la
   paletta dei colori: tredici pastiglie sempre aperte dentro un terzo di riga
   andavano a capo tre volte, e una griglia di colori che si spezza smette di
   leggersi come una scelta fra pari. Da quando la paletta è chiusa dietro un
   grilletto largo come un `<select>`, quel problema non esiste e nessun template
   la nomina più.

   Una regola che non veste niente è la prossima `.form-panel .field`: la si
   trova viva, ci si appoggia, e nessuno si accorge che non fa quello che dice.
   Se un domani serve di nuovo, sono due righe. */

.form-panel__actions {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
  gap: var(--s-3);
  padding-top: var(--s-4);
  border-top: 1px solid var(--c-border);
}

/* ⛔ QUI C'ERANO I DUE `@media` DELLA GRIGLIA — 900px e 640px, andati via
   l'11-08-2026 e non sostituiti: le tre forme le decide ora `@container`, sopra.
   Tenerli entrambi non era un doppione innocuo, era una contraddizione — su una
   finestra da 1920px il `@media` diceva «tre colonne» a un pannello che il
   lettore aveva appena stretto a 365px, ed è esattamente il difetto. */

@media (max-width: 640px) {
  /* One action per row, full width: a row of small buttons at the bottom of a
     phone screen is a row of misses.
     Sulla FINESTRA e non sul contenitore, deliberatamente: questa barra vive
     nella pagina del profilo, dove non c'è nessun pannello da stringere. Quella
     del dialogo è `.modal__actions`, vestita in dialog.css. */
  .form-panel__actions > .button { flex: 1 1 100%; }
}

/* ================================================================= profilo
 * core/pages/profile.html — one's own record, read-only, and the one form on
 * it: the password.
 *
 * Two cards and not one, and the CSS follows the markup rather than fighting
 * it: the record is read, the password is acted on. A single card would put a
 * «Salva» at the bottom of both.
 *
 * ⛔ QUI C'ERA `var(--s-5)`, E `--s-5` NON ESISTE — corretto il 2026-08-10.
 *
 * La scala salta il 5: `--s-1 --s-2 --s-3 --s-4 --s-6 --s-8 --s-12`, riga 168.
 * Una `var()` verso un token non dichiarato **non è un errore che si vede**: la
 * dichiarazione è invalid at computed-value time, quindi il browser la butta e
 * la proprietà torna al proprio valore iniziale. `gap` tornava a `normal` (zero
 * su un flex) e il `padding-top` del blocco qui sotto tornava a zero — cioè le
 * tre parti della scheda si toccavano e la linea di separazione stava attaccata
 * alle etichette che introduce. Nessun avviso, nessun test rosso, e in DevTools
 * la riga si legge come scritta: bisogna guardare il valore calcolato.
 *
 * Presidiato da `test_every_css_token_used_is_declared` in
 * `tests/test_scaffold.py`: è il `⛔MANCA` dei fogli di stile.
 */
.profile { display: flex; flex-direction: column; gap: var(--s-6); }

.profile__identity { display: flex; align-items: center; gap: var(--s-4); }

/* Bigger than the header chip's avatar and the register's: here it is the
   subject of the page rather than a marker in a row. */
.profile__avatar {
  width: 56px; height: 56px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  background: var(--c-primary);
  font-size: 1.35rem; font-weight: 700; color: #fff;
}
.profile__identity-text { min-width: 0; }
.profile__name {
  margin: 0;
  font-size: 1.15rem; font-weight: 600; color: var(--c-text-strong);
}
.profile__role {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  margin: var(--s-1) 0 0;
  color: var(--c-text-muted);
}

/* A <dl> laid out as a grid: `auto-fit` and not a fixed count, so the four
   facts fill the card at 1440px and stack at 375px without a media query.

   La linea è un separatore, e lo spazio intorno non è simmetrico di proposito:
   24px sopra (il `gap` della scheda) e 16px sotto. Una riga di separazione
   **introduce** ciò che le sta sotto, quindi sta più vicina a quello; a spazio
   uguale sembrerebbe sospesa fra due blocchi senza appartenere a nessuno.

   `--s-4` è anche il valore dell'altro separatore dell'applicazione,
   `.form-panel__actions`: le due linee dell'interfaccia respirano uguale.
   Era `var(--s-5)`, che non esiste — vedi il blocco sopra. */
.profile__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s-4);
  margin: 0;
  padding-top: var(--s-4);
  border-top: 1px solid var(--c-border);
}
.profile__fact dt {
  font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--c-text-muted);
}
.profile__fact dd { margin: var(--s-1) 0 0; color: var(--c-text-strong); }

/* La nota in fondo alla scheda. `.profile` è un flex e lo spazio lo dà il suo
   `gap`: il margine che un `<p>` si porta dal foglio del browser ci si somma,
   e i 24px diventavano 38 sopra la nota e 38 sotto — cioè la scheda con più
   aria in fondo che in cima, misurata.

   Selettore stretto — `.profile > .muted` e non `.muted` — perché la stessa
   classe veste anche il paragrafo sotto «Cambia la password», che sta in una
   scheda NON flex: là quel margine è l'unica spaziatura che c'è, e toglierlo
   attaccherebbe il testo al titolo. */
.profile > .muted { margin: 0; }

/* The password fields are stacked and NOT in the registry's three-column grid:
   two password boxes side by side read as one control repeated, and the order
   — new, confirm — is the instruction. Capped, because a password input as wide
   as a 1440px card is a target far larger than what it holds.

   ⚠ IL `gap` È OBBLIGATORIO, e la sua assenza è costata una schermata storta —
   2026-08-10. `.form-panel__form .field { margin-bottom: 0 }` toglie al campo il
   proprio margine perché nel dialogo lo spazio lo dà la griglia; questo
   contenitore però non è una griglia, e senza `gap` i campi si toccavano: il
   bordo inferiore di un input **esattamente** sul bordo superiore
   dell'etichetta successiva, misurato 0px. Non era un difetto di gusto — era la
   regola del margine applicata a un contenitore che non aveva l'altra metà.

   `--s-4` e non un valore nuovo: è lo stesso passo dei campi impilati del login
   (`.field` con il suo margine) e delle righe della griglia del dialogo. Le tre
   forme di campi impilati dell'applicazione respirano uguale. */
.profile__fields {
  display: flex; flex-direction: column; gap: var(--s-4);
  max-width: 26rem;
}

/* ====================================================== bare layout + login
 * base_bare.html and core/pages/login.html — the pages that exist before a
 * session does.
 */

.bare__main {
  flex: 1;
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-6);
}
.bare__footer {
  padding: var(--s-4) var(--s-6);
  text-align: right;
  color: var(--c-text-muted); font-size: 0.8rem;
}
.bare__version { font-family: ui-monospace, monospace; }

.login { width: 100%; max-width: 26rem; }

.login__head { margin-bottom: var(--s-6); text-align: center; }
.login__brand {
  margin: 0;
  font-size: 1.375rem; font-weight: 700; letter-spacing: -0.5px;
  color: var(--c-text-strong);
}
.login__brand-accent { color: var(--c-primary); }
.login__title { margin: var(--s-4) 0 var(--s-3); }

/* Which workshop is this. Read from a couple of steps back, standing up, so
   it is a pill and not fine print. */
.login__installation {
  display: inline-block; margin: 0;
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-pill);
  background: var(--c-primary-soft); color: var(--c-primary);
  font-size: 0.95rem; font-weight: 600;
}

/* Gloves: the two fields and the button are taller than the standard control,
   and the button is the full width of the card — it cannot be missed and
   cannot be mistaken for anything else. */
.login__form .field input { min-height: 52px; font-size: 1.05rem; }
.login__submit {
  width: 100%; min-height: 56px; margin-top: var(--s-2);
  font-size: 1.05rem;
}

.login__note {
  margin: var(--s-4) 0 0;
  text-align: center; font-size: 0.85rem;
}

/* ------------------------------------------------------ login, narrow view
 * Verificata a 375px. Il dialogo aveva la sua parte qui e se l'e' portata via
 * in dialog.css, dove la versione a foglio (sheet) e' scritta nel linguaggio
 * dello strato iOS invece che approssimata in questo.
 */
@media (max-width: 640px) {
  .bare__main { padding: var(--s-4); }
  .bare__footer { padding: var(--s-3) var(--s-4); }
}

/* ========================================================== side navigation
 * components/_sidenav.html, included by base.html. Fed by nav_sections
 * (apps/core/navigation.py) — this file styles a menu and knows no more about
 * what is in it than the component does.
 *
 * MOBILE-FIRST HERE, AND FOR A REASON. The rules below are the narrow
 * viewport: an off-canvas drawer. The docked sidebar arrives in a min-width
 * query at the end. Written this way round because the drawer is the state
 * that must survive a media query that never matches — a browser that ignores
 * the query gets a working page with a closed menu, not a 15rem column
 * eating a 375px screen.
 */

.app-header__nav-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--touch-min); min-height: var(--touch-min);
  padding: 0; font-size: 1.15rem; line-height: 1;
  background: transparent; color: var(--c-text-strong);
  border: 1px solid var(--c-border); border-radius: var(--r-md);
  cursor: pointer;
}
.app-header__nav-toggle:hover { border-color: var(--c-primary); }

.sidenav {
  position: fixed;
  top: var(--header-h); bottom: 0; left: 0;
  width: min(17rem, 82vw);
  z-index: 30;
  overflow-y: auto;
  /* Horizontal padding on the rail and not on the items: in v3 the current
     entry is a rounded pill, and a pill needs air on both sides of itself —
     with the padding on the item, the highlight would run into the edge of the
     panel. */
  padding: var(--s-4) var(--s-3);
  background: var(--c-surface);
  border-right: 1px solid var(--c-border);
  /* Off-canvas, not display:none: a drawer that slides tells the reader where
     it came from and where it goes back to. transform and not `left` because
     only transform is composited — on the workshop terminal that is the
     difference between a slide and a stutter. */
  transform: translateX(-100%);
  transition: transform var(--t-fast);
}
.sidenav--open { transform: translateX(0); }

/* Hidden by default IN CSS, shown by a class Alpine binds. Not `x-show`: the
   default state then depends on a deferred script having run, and the component
   comment records what that cost when it was tried. */
.sidenav__scrim {
  display: none;
  position: fixed; inset: var(--header-h) 0 0 0;
  z-index: 25;
  background: var(--c-scrim);
}
.sidenav__scrim--shown { display: block; }

.sidenav__nav { display: flex; flex-direction: column; gap: var(--s-6); }

.sidenav__section { display: flex; flex-direction: column; gap: var(--s-1); }

/* A real <h2>, so the menu is navigable by headings. The capitals are here and
   not in the markup: a screen reader announces «OFFICINA» written in the text
   letter by letter, and «Officina» styled uppercase as a word. */
.sidenav__title {
  margin: 0 0 var(--s-1); padding: 0 var(--s-3);
  font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--c-text-muted);
}

.sidenav__list { list-style: none; margin: 0; padding: 0; }

.sidenav__item {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: var(--touch-min);
  padding: var(--s-2) var(--s-3);
  border: 0; border-radius: var(--r-md);
  color: var(--c-text); text-decoration: none;
  font-size: 0.85rem;
}

/* The square dot in front of every entry. Decoration, and marked aria-hidden in
   the component — but decoration that does one useful thing: it is where the
   current-item colour lands, so «where am I» survives being read by someone who
   cannot tell the amber wash from white. */
.sidenav__dot {
  width: 8px; height: 8px; flex-shrink: 0;
  border-radius: 3px;
  background: var(--c-border-strong);
}

/* Amber, not violet, and it is the design's choice, not a preference: in v3 —
   and in the platform this design comes from — violet is the colour of an
   action you take, amber the colour of the place you are. */
a.sidenav__item:hover { background: var(--c-accent-soft); color: var(--c-accent-strong); }

.sidenav__item--current {
  background: var(--c-accent-tint);
  color: var(--c-accent-strong);
  font-weight: 600;
}
.sidenav__item--current .sidenav__dot { background: var(--c-accent); }

/* An entry whose screen does not exist yet. It is a <span>: nothing to
   disable, nothing to focus, nothing that fails when pressed. The cursor stays
   the default arrow — a pointer would be a promise. */
.sidenav__item--soon { color: var(--c-text-muted); cursor: default; }

/* Small on purpose: it is a caveat, not a label, and at the size it had it took
   enough room from the entry to wrap «Tipologie asset» onto two lines. */
.sidenav__note {
  margin-left: auto; flex-shrink: 0;
  padding: 2px 6px;
  border-radius: var(--r-pill);
  background: var(--c-neutral-soft); color: var(--c-neutral);
  font-size: 0.62rem; font-weight: 600; white-space: nowrap;
}

@media (min-width: 60rem) {
  /* Docked. The toggle disappears because there is nothing left to toggle. */
  .app-header__nav-toggle { display: none; }

  .sidenav {
    position: sticky;
    inset: auto; top: var(--header-h);
    align-self: flex-start;
    flex: 0 0 var(--sidenav-w);
    width: var(--sidenav-w);
    /* Full viewport height, not the height of its own entries. `align-self:
       flex-start` is what makes `sticky` work here — stretching it to the shell
       would make it as tall as the page and it would stop sticking — but on its
       own it leaves the panel ending halfway down the screen, which reads as a
       floating box rather than as the edge of the application. */
    min-height: calc(100vh - var(--header-h));
    max-height: calc(100vh - var(--header-h));
    transform: none;
    z-index: auto;
  }

  /* The drawer's backdrop must not survive into the docked layout: `navOpen`
     can still be true — someone opened the drawer and then widened the window
     — and a scrim over a page nobody asked to dim is a page that looks
     broken. Both selectors, because the shown state is a class and would
     otherwise win on specificity. */
  .sidenav__scrim,
  .sidenav__scrim--shown { display: none; }
}

/* ==================================================== operators register
 * operators/pages/list.html and its partials.
 */

/* The two groups explained next to the filter that uses them. Not a table:
   it is two sentences, and a table of two rows and two columns reads as data
   the reader is meant to compare. */
.group-legend { display: flex; flex-wrap: wrap; gap: var(--s-4); }
.group-legend__item { flex: 1 1 16rem; }
.group-legend__name { margin: 0 0 2px; font-weight: 700; color: var(--c-text-strong); }
.group-legend__opens { margin: 0; font-size: 0.9rem; color: var(--c-text-muted); }

/* Row actions. A column of small buttons, and the minimum touch target still
   applies: this screen is used at a desk, but the same components end up on
   the terminal and a 28px button is a 28px button everywhere.
 *
 * Outlined and tinted, as v3 draws them: three solid buttons per row, sixteen
 * rows, and the table stops being a table and becomes a wall of buttons. The
 * border carries the meaning — violet for «go on», red for «take away» — and
 * the shadow of the page-level CTAs is deliberately absent here: what has to
 * stand out on this screen is «+ Nuovo operatore», once.
 */
.row-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); justify-content: flex-end; }
.row-actions .button {
  min-height: 36px; padding: 0 var(--s-3); font-size: 0.78rem;
  border-radius: var(--r-sm);
  border: 1.5px solid var(--c-primary-soft);
  background: var(--c-surface); color: var(--c-primary);
  box-shadow: none;
}
.row-actions .button:hover { border-color: var(--c-primary); background: var(--c-primary-soft); }

.row-actions .button--danger {
  border-color: var(--c-danger-soft);
  background: var(--c-surface); color: var(--c-danger);
}
.row-actions .button--danger:hover { border-color: var(--c-danger); background: var(--c-danger-soft); }

/* The badge cell: a small dark square that says «this is a printed label», then
   the code in the monospace. The glyph is the one v3 uses and it is decoration —
   aria-hidden in the template — because the code beside it is the information. */
.cell-badge { display: inline-flex; align-items: center; gap: var(--s-2); white-space: nowrap; }
.cell-badge__icon {
  width: 22px; height: 22px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 5px;
  background: var(--c-text-strong); color: var(--c-surface);
  font-size: 0.7rem; line-height: 1;
}
.cell-badge__code { font-weight: 600; color: var(--c-text-strong); }

/* A suspended operator, in the register. Not hidden and not struck through:
   the row is still readable and still actionable — it has to be, because
   putting the person back in service is done from it.

   `.row--inactive` says the same thing without naming one registry: it is what
   the customers register uses for a retired company, and what the three
   registries still to come will use. `.operator-row--suspended` stays because
   the word is not the same — a person is suspended, a company is retired — and
   because renaming a class that a browser test aims at buys nothing. */
.operator-row--suspended td,
.operator-row--suspended th,
.row--inactive td,
.row--inactive th { color: var(--c-text-muted); }
.operator-row--suspended .cell-name { text-decoration: none; }

/* Una riga in ritardo, nella flotta a noleggio (RF-078). Il rosso tenue del
   progetto e non il #FDF7F7 del prototipo: sono due rosa quasi uguali, e due
   quasi-uguali su una schermata sono il modo in cui un prodotto smette di
   sembrare uno.

   Il fondo sta sulle celle e non sulla riga perché `<tr>` in una tabella non
   dipinge il proprio sfondo sotto le celle: `background` su una riga si vede
   solo dove non c'è una cella, cioè da nessuna parte.

   ⚠️ Deve stare DOPO `.row--inactive`: una macchina dismessa non è in ritardo,
   quindi le due classi non si incontrano mai sulla stessa riga — ma se un giorno
   si incontrassero, l'ordine di dichiarazione è ciò che decide, e il ritardo è
   l'informazione che va vista. */
.row--late td,
.row--late th { background: var(--c-danger-soft); }
/* La data di rientro scaduta. Grassetto E colore: il colore da solo è
   l'informazione che non arriva a chi non lo distingue. */
.cell-overdue { color: var(--c-danger); font-weight: 700; }

/* La giacenza a magazzino, dipinta — 2026-08-24, dal prototipo v3 (riga 1728).
   Due classi e non tre: «a posto» è il colore normale del testo, e una classe che
   dichiara il valore predefinito è una classe che un giorno smette di combaciare
   con lui.

   ⛔ IL COLORE STA SUL NUMERO E NON SULLA RIGA, ed è un cambio di rotta rispetto
   a come questa tabella era nata: usava `.row--late`, che tinge di rosa tenue
   TUTTE le celle. Su una flotta a noleggio è giusto — un rientro in ritardo è un
   fatto sulla macchina intera — ma in magazzino «sotto scorta» è un fatto su UN
   numero, e dipingere la riga intera dice che c'è qualcosa che non va
   nell'articolo. Con dieci righe su venticinque sotto soglia, che in un magazzino
   vero è normale, la tabella diventa rosa e l'avviso in testa non si vede più.

   Grassetto E colore su entrambe, per la stessa ragione di `.cell-overdue`
   sopra: il colore da solo è l'informazione che non arriva a chi non lo
   distingue. Qui la sostiene anche la pastiglia «Esaurito»/«Sotto scorta» nella
   colonna «Stato», che è testo e non ha bisogno di essere vista in colore. */
/* `.cell-waiting` sta nella stessa dichiarazione e non in una propria: è la
   colonna «Atteso da» della stessa tabella, ed è lo stesso ambra per la stessa
   ragione — «qui c'è qualcosa da fare». Il prototipo usa lo stesso colore nelle
   due celle (#8A4B15, righe 1728 e 1731). Due regole identiche con due nomi sono
   il modo in cui un giorno una delle due cambia da sola. */
.cell-stock--low,
.cell-waiting    { color: var(--c-warn-strong); font-weight: 700; }
.cell-stock--out { color: var(--c-danger);      font-weight: 700; }

.cell-name { display: flex; align-items: center; gap: var(--s-3); }
.cell-name__text { display: flex; flex-direction: column; }
.cell-name__full { font-weight: 600; color: var(--c-text-strong); }
/* `__username` is the operators register's own name for its second line; every
   other table has one that is not a username — a city, a telephone number, the
   type of a machine — and `__sub` is that line. One declaration for both: two
   secondary lines that do not look alike are two tables that do not look alike. */
.cell-name__username,
.cell-name__sub { font-size: 0.78rem; color: var(--c-text-muted); }

/* Un gruppo di caselle dentro un `.field` — oggi «Tipo cliente», domani ogni
   scelta multipla che arriverà. Serve perché la regola condivisa dei controlli
   (`.field input`, più in alto) è scritta per una casella che aspetta una
   frase: larghezza piena, 44px di altezza, bordo proprio. Applicata a una
   checkbox produce un rettangolo grigio con la spunta persa dentro.

   Il bersaglio è l'etichetta intera e non il quadratino: 20px di lato non sono
   un bersaglio, e `min-height` sulla riga tiene la misura minima del tocco
   anche qui, dove il controllo è piccolo per natura. */
.choice-list { display: flex; flex-wrap: wrap; gap: var(--s-4); }
.choice {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: var(--touch-min);
  cursor: pointer;
}
/* ⚠ E ANCHE I RADIO, dal 2026-08-25 (la scelta dell'etichetta in accettazione).
   Senza `[type="radio"]` qui dentro, un radio in una `.choice` ereditava la
   regola `.field input` — bordo, padding, altezza di una casella di testo — e il
   pallino spariva dentro un rettangolo alto 44px. */
.choice input[type="checkbox"],
.choice input[type="radio"] {
  /* `appearance: auto` e `border: 0` riportano la casella nativa: senza,
     eredita il bordo e lo sfondo del campo di testo e la spunta scompare. */
  appearance: auto;
  width: 20px; height: 20px; min-height: 0; flex-shrink: 0;
  padding: 0; border: 0;
  accent-color: var(--c-primary);
}
.field--invalid .choice input[type="checkbox"] { accent-color: var(--c-danger); }

@media (max-width: 640px) {
  /* nowrap, and it makes the rows SHORTER, not wider in any way that costs
     anything: the actions column is the last one of a table that already scrolls
     sideways, so three buttons on one line simply live a little further right.
     Wrapped, they stacked three deep and every row of the register became 100px
     tall — measured at 375px, where it meant four operators on a screen. */
  .row-actions { justify-content: flex-start; flex-wrap: nowrap; }

  /* The heading and its loading indicator, wrapping if they must. The rule for a
     button in here went away with the button: «+ Nuovo operatore» moved to the
     page head on 2026-08-06, where the page-level action belongs. */
  .orders-panel__head { flex-wrap: wrap; }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * IL COMPONENTE GRIGLIA — ADR-0020, 2026-08-29
 *
 * Due cose, chieste insieme dal committente: lo scorrimento verticale DENTRO la
 * griglia (testata e piede fermi) e la tendina «righe per pagina» accanto al
 * paginatore. Più una terza, chiesta guardando la prima consegna: **almeno
 * dieci righe** appena si apre la pagina.
 *
 * 🔄 QUARTA FORMA, E LE PRIME TRE SONO STATE SBAGLIATE. Vale la pena elencarle,
 * perché sono tre modi diversi di sbagliare la stessa cosa e la quarta si
 * capisce solo sapendo da cosa scappa:
 *
 *   1ª — guscio a schermo fisso e `flex: 1` sulla griglia: «riempi il vuoto che
 *        resta». ⛔ Non è una specifica: dipende da quanto vuoto c'è, e sulla
 *        dashboard sopra la griglia stanno 435px di avvisi e KPI. Restava
 *        **una riga**.
 *   2ª — pavimento di dieci righe + `overflow: visible` sulla card per non far
 *        tagliare il piede. ⛔ Una card che non contiene le proprie righe **non
 *        le colora**: il fondo cambiava a metà tabella.
 *   3ª — pannello `flex: none` e colonna `.stack` che scorre. ⛔ Ha portato una
 *        **barra orizzontale** che nessuno voleva (impostare un solo asse di
 *        `overflow` fa computare `auto` all'altro) e la pagina continuava a
 *        scorrere lo stesso, perché `overflow: hidden` su `<body>` si propaga al
 *        viewport e lascia il corpo traboccare.
 *
 * ⭐ LA QUARTA NON HA GUSCIO. Nessun `app--fixed`, nessuna catena di anelli
 * flex, nessuna colonna che scorre: **una misura sola, su un elemento solo**.
 * `.grid__scroll` è alto fra un pavimento e un soffitto, e dentro scorre. Tutto
 * il resto della pagina sta nel flusso normale — quindi la card contiene sempre
 * le sue righe, nessun antenato traboccca, e non c'è nessuna barra di troppo.
 *
 * ⛔ QUESTE REGOLE STANNO IN `app.css` E NON IN `components.css`, ed è una
 * scelta: `app.css` è collegato direttamente da `base.html` e sta FUORI dalla
 * catena di `app.tailwind.css`, quindi non c'è niente da ricompilare — vedi
 * ripresa.md §28. Qui vivono già `.app-main`, `.table-scroll`, `.data-table` e
 * `.pager`.
 * ═══════════════════════════════════════════════════════════════════════════ */

.grid { display: flex; flex-direction: column; }

.grid__scroll {
  /* ⭐ IL PAVIMENTO: DIECI RIGHE, SEMPRE — richiesta del committente, 2026-08-29:
     «di default mi devi mostrare almeno 10 righe, e poi scrollo per continuare a
     vedere le altre».

     ⚠️ I NUMERI SONO MISURE, NON STIME: la riga della dashboard è alta **61,8px**
     (due righe nella cella del codice) e la testata **32,8px**, prese in
     Chromium. `3.875rem` = 62px, e `2.5rem` copre la testata con un margine. Le
     altre otto griglie hanno righe più basse, quindi lì il pavimento ne mostra
     qualcuna in più: è un minimo, non una misura esatta.

     ⛔ NON SI ABBASSA per far entrare una finestra bassa. Se dieci righe non ci
     stanno, scorre la pagina — non nove righe, poi otto, poi una, che è da dove
     si è partiti. */
  --grid-min-rows: 10;
  --grid-row-h: 3.875rem;
  min-height: calc(var(--grid-min-rows) * var(--grid-row-h) + 2.5rem);

  /* ⭐ E IL SOFFITTO, che è ciò che tiene le RIGHE dentro la griglia invece che
     sulla pagina — la richiesta di partenza. `22rem` è il contorno: testata
     della pagina, avvisi, KPI, filtri e piede.

     ⚠️ È UNA STIMA, ed è l'unica di questo blocco: sbagliarla per eccesso
     accorcia un po' la griglia, per difetto fa scorrere la pagina qualche pixel
     in più. Nessuno dei due rompe niente — ed è la ragione per cui qui una
     stima è accettabile e nel pavimento no. */
  max-height: calc(100dvh - 22rem);

  overflow: auto;

  /* ⛔ `contain: paint` VIENE TOLTO, e `.table-scroll` invece lo dichiara — lo
     stesso elemento porta tutte e due le classi. Serviva a contenere il
     traboccamento ORIZZONTALE, e a quello basta `overflow-x`, che c'è. Adesso
     fa danno: questo riquadro scorre anche in VERTICALE e contiene intestazioni
     `position: sticky`, e su quella combinazione la testata si ridipinge a ogni
     fotogramma. ⚠️ Toglierlo NON riapre il ritaglio del menu di riga: quello
     vive nel top layer (`popover`), e `contain: paint` per lui era un pericolo
     dichiarato, non un aiuto → tests/test_row_menu_browser.py. */
  contain: none;

  /* Lo scorrimento non prosegue sulla pagina mentre si sta ancora scorrendo le
     righe. ⚠️ Vale DENTRO un gesto: Chrome riaggancia all'antenato a ogni nuova
     rotellata, e questa regola non lo impedisce né deve — o la pagina
     diventerebbe irraggiungibile dalla griglia. */
  overscroll-behavior: contain;

  /* Lo slancio su iOS: senza, dentro un contenitore che scorre il dito si ferma
     di colpo — su un tablet in officina è la differenza fra «scorre» e «è
     rotto». */
  -webkit-overflow-scrolling: touch;
}

/* ⭐ LA TESTATA DELLE COLONNE RESTA FERMA MENTRE LE RIGHE SCORRONO, ed è la metà
   della richiesta che senza questa riga non si vede: dopo dieci righe nessuno sa
   più quale colonna stia leggendo.

   `position: sticky` sul `<th>` e NON sul `<thead>`: in una tabella il `<thead>`
   non è un contenitore posizionabile in tutti i browser, e la riga che «funziona
   su Chrome» è esattamente quella che non funziona altrove. Lo sfondo è
   obbligatorio — un `th` trasparente lascia passare le righe sotto di sé. */
.grid__scroll .data-table thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--c-surface-alt);
}

/* ── Il piede: tendina a sinistra, paginatore a destra ──────────────────────
 *
 * `margin-left: auto` sul `.pager` è già in questo foglio: è ciò che spinge la
 * paginazione a destra e lascia la tendina dov'è. Qui non si ripete. */
.grid-foot {
  display: flex; align-items: center; gap: var(--s-3);
  flex-wrap: wrap;
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--c-border);
}
.grid-foot__size { display: flex; align-items: center; gap: var(--s-2); margin: 0; }
.grid-foot__label { font-size: 0.85rem; color: var(--c-text-muted); }
.grid-foot__select {
  min-height: var(--touch-min);
  padding: 0 var(--s-2);
  border: 1px solid var(--c-border); border-radius: var(--r-sm);
  background: var(--c-surface); color: var(--c-text);
  font: inherit; font-size: 0.85rem;
}
/* ⛔ NON È CODICE MORTO. Il tasto esiste perché senza JavaScript l'`hx-get`
   non parte e la tendina sarebbe un ornamento: è la strada di chi non ha
   JavaScript, e riappare da sé se questo foglio non arriva. */
.grid-foot__go { display: none; }

/* ── Sotto i 768px la griglia torna alta quanto le sue righe ────────────────
 *
 * ⛔ E NON È UNA RINUNCIA. Su un telefono un riquadro che scorre dentro una
 * pagina che scorre è una trappola: il dito parte dentro la griglia e la pagina
 * sembra bloccata. Là scorre la pagina, che è quello che il pollice si aspetta.
 * ⚠️ Vanno via TUTTE E DUE le misure: 660px di pavimento su un telefono alto 812
 * lascerebbero fuori ciò che sta sopra la griglia, e un soffitto di `100dvh`
 * meno 22rem sarebbe negativo. */
@media (max-width: 768px) {
  .grid__scroll {
    min-height: 0; max-height: none;
    overflow-x: auto; overflow-y: visible;
  }
  .grid__scroll .data-table thead th { position: static; }
}
