/* ==========================================================================
   Quercia viva — Glass components (brand §5)
   Re-skins the existing Tabler classes so current usages upgrade without
   touching the ~126 templates.

   Buttons are written as property overrides rather than through Tabler's
   --tblr-btn-* variables, and the reason is the recipe, not the framework: a
   glass button is a gradient over a blur, and --tblr-btn-bg holds one flat
   colour. Everything that IS a single value — radius, weight, focus ring — goes
   through the variables, so only what cannot be expressed there is restated.
   (An older note here blamed Tabler for compiling button colours as literals.
   It never did: beta19 already wrote --tblr-btn-bg: var(--tblr-primary).)

   CONTRAST NOTE: recipe §5.1 uses a translucent primary fill (72–88%). On
   our light neutral `background` (#F5F6F1) white text would drop below 4.5:1. So:
     - FILLED buttons (primary/semantic): solid gradient tint→dark, white
       text → guaranteed >= 5:1 contrast;
     - SECONDARY/OUTLINE buttons: true "glass" (translucent fill + blur),
       primary-colored text on the light page → safe contrast.
   ========================================================================== */

/* ------------------------------------------------------------------ *
 * Visible focus (a11y) — golden rule
 * ------------------------------------------------------------------ */
:focus-visible {
  outline: 2px solid var(--qc-primary);
  outline-offset: 2px;
}
/* One sign each. What is typed into or ticked wears a primary edge and the
   halo, and no outline; a button — and everything else — wears the outline
   above and no halo. Buttons used to carry both, and Tabler's own focus
   shadow replaced a filled button's bevel while it had the focus. */
.form-control:focus,
.form-select:focus,
.form-check-input:focus {
  outline: 0;
  box-shadow: var(--qc-focus-ring);
  border-color: var(--qc-primary);
}
/* Stated on the button itself: Tabler's `.btn:focus-visible` sets `outline: 0`
   and outweighs the bare rule above. */
.btn:focus-visible {
  outline: 2px solid var(--qc-primary);
  outline-offset: 2px;
  box-shadow: var(--qc-btn-shadow, none);
}

/* ------------------------------------------------------------------ *
 * The scrollbar's strip is always there, whether or not the scrollbar is
 * ------------------------------------------------------------------ *
 * A page that fits and a page that does not are two different widths on any
 * platform with classic scrollbars — Windows, most Linux desktops, and a Mac
 * whose owner has set "Show scroll bars: Always". The content column is
 * `margin: 0 auto` inside that width, so it re-centres on the difference:
 * measured at 800px against 785px, a page opening after a long one moves its
 * whole body sideways by half of fifteen pixels and back again on the way out.
 * `stable` reserves the strip on both kinds of page, so there is one width.
 *
 * (On a Mac with the default overlay scrollbars there was never a difference to
 * remove, and this changes nothing there. It is written for everybody else.)
 *
 * The two `!important` below are the price, and they are aimed at exactly one
 * thing. Bootstrap locks the page behind a modal or an offcanvas by hiding the
 * body's overflow, and compensates for the scrollbar it expects to have removed
 * — `padding-right` on <body>, a negative `margin-right` on `.sticky-top` —
 * sized from `innerWidth - documentElement.clientWidth`. That measurement does
 * not see a reserved gutter: it still reads 15 while the gutter goes on holding
 * the 15 open, so the compensation is applied twice and the page narrows by
 * that much every time a dialog opens. With a stable gutter the correct
 * compensation is always zero. Nothing else in this app writes either property,
 * and anything that wants to should not use these two names.
 * ------------------------------------------------------------------ */
html { scrollbar-gutter: stable; }
body { padding-right: 0 !important; }
.sticky-top { margin-right: 0 !important; }

/* ================================================================== *
 * §5.1 — Action buttons: a filled one with a bevel, a secondary one with a wash
 * ================================================================== */
.btn {
  /* Stated through Tabler's own button variables, not as flat properties:
     button groups, input groups and the sizing modifiers all read these names,
     so they follow along instead of each needing a rule of its own. The radius
     is given explicitly because Tabler defaults it to --tblr-border-radius,
     which the token layer points at the tighter field radius. */
  --tblr-btn-border-radius: var(--qc-radius-btn);
  --tblr-btn-font-family: var(--qc-font-body);
  --tblr-btn-font-weight: 600;
  transition: transform var(--qc-dur-press) var(--qc-ease-press),
              box-shadow var(--qc-dur-fast) var(--qc-ease-fast),
              background-color var(--qc-dur-fast) var(--qc-ease-fast),
              border-color var(--qc-dur-fast) var(--qc-ease-fast);
}
.btn:active { transform: scale(.97); }

/* --- Filled buttons: lit from above, ink chosen per tint ---
   The tint, a shade lighter at the top; an edge of the tint darkened; a line of
   light inside the top edge and a line of shade inside the bottom one; a short
   contact shadow. No coloured glow under it: a button that glows competes with
   the one beside it. The ink holds at both ends, at rest and under the pointer
   (≥4.8:1; white on forest is the tightest). */
.btn-primary,
.btn-success,
.btn-danger,
.btn-warning {
  --qc-btn-fg: #fff;
  color: var(--qc-btn-fg);
  border: 1px solid color-mix(in srgb, var(--qc-btn-tint) 78%, #000);
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--qc-btn-tint) 92%, #fff),
      var(--qc-btn-tint));
  --qc-btn-shadow: inset 0 1px 0 rgb(255 255 255 / .28),
                   inset 0 -1px 0 rgb(0 0 0 / .18),
                   0 1px 2px var(--qc-shadow-color);
  box-shadow: var(--qc-btn-shadow);
}
/* on-primary, not #fff: primary is forest in light and mint in dark, and the
   brand pins the ink for each (#FFFFFF, then near-black green #04120A). The
   block above defaults every filled button to white, which is right for the
   two semantic tints but put white on mint for the whole dark theme — about
   1.7:1 on the button somebody presses most. */
.btn-primary { --qc-btn-tint: var(--qc-primary); --qc-btn-fg: var(--qc-on-primary); }
/* Success is a light green in both themes: white on it was 2.4:1, near-black
   holds from the tint to the dark end of the gradient (7.7 → 5.0). */
.btn-success { --qc-btn-tint: var(--qc-success); --qc-btn-fg: rgba(0, 0, 0, .87); }
/* No ink holds at both ends of a gradient started from --qc-error (#EF5350):
   white fails at the tint, near-black at the dark end. The fill is the text-safe
   red instead — deep in light, rose in dark — with the ink primary already pins
   for each theme (6.1 → 8.6 in light, 7.9 → 5.0 in dark). */
.btn-danger  { --qc-btn-tint: var(--qc-error-text); --qc-btn-fg: var(--qc-on-primary); }
.btn-warning { --qc-btn-tint: var(--qc-warning); --qc-btn-fg: rgba(0, 0, 0, .87); color: rgba(0, 0, 0, .87); }

.btn-primary:hover,
.btn-success:hover,
.btn-danger:hover,
.btn-warning:hover {
  color: var(--qc-btn-fg);
  border-color: color-mix(in srgb, var(--qc-btn-tint) 78%, #000);
  background: linear-gradient(180deg,
      color-mix(in srgb, var(--qc-btn-tint) 88%, #fff),
      color-mix(in srgb, var(--qc-btn-tint) 96%, #fff));
}

/* --- Secondary buttons: a wash of primary, and no blur ---
   They were "glass": a tinted gradient with a `backdrop-filter`. On a flat page
   the blur had nothing to blur, and there is one of these on every row of a
   table. A wash over whatever it stands on, an edge, and the label in the ink
   that holds on it — 5.7:1 or more on the page, a card, a well and an overlay,
   at rest and under the pointer (the green label on the old wash was 4.47:1 in
   light). Real blur stays where rule 3 allows it: icon buttons, overlays. */
.btn-secondary,
.btn-outline-primary,
.btn-outline-secondary,
.btn-info {
  --qc-btn-tint: var(--qc-primary);
  color: var(--qc-primary-ink);
  border: 1px solid color-mix(in srgb, var(--qc-btn-tint) 30%, transparent);
  background: color-mix(in srgb, var(--qc-btn-tint) 10%, transparent);
  box-shadow: none;
}
/* The red of an error is a fill (#EF5350); as text it was 3.3–3.8:1 on its own
   wash. The label wears --qc-error-text, and the wash is thinner than the green
   one so that it holds on a card, on the page and in a well (≥4.5:1 on each).
   Tabler's own variables are set too: its :hover and :active would otherwise
   fill the button with its red and print white on it. No blur — the row of a
   table is where this button lives. */
.btn-outline-danger {
  --qc-btn-tint: var(--qc-error);
  --tblr-btn-color: var(--qc-error-text);
  --tblr-btn-hover-color: var(--qc-error-text);
  --tblr-btn-active-color: var(--qc-error-text);
  --tblr-btn-hover-bg: transparent;
  --tblr-btn-active-bg: transparent;
  --tblr-btn-hover-border-color: color-mix(in srgb, var(--qc-error-text) 55%, transparent);
  --tblr-btn-active-border-color: color-mix(in srgb, var(--qc-error-text) 55%, transparent);
  color: var(--qc-error-text);
  border: 1.2px solid color-mix(in srgb, var(--qc-error-text) 34%, transparent);
  background: linear-gradient(135deg,
      color-mix(in srgb, var(--qc-btn-tint) 9%, transparent),
      color-mix(in srgb, var(--qc-btn-tint) 5%, transparent));
  box-shadow: none;
}
.btn-outline-danger:hover,
.btn-outline-danger:active {
  color: var(--qc-error-text);
  background: linear-gradient(135deg,
      color-mix(in srgb, var(--qc-btn-tint) 12%, transparent),
      color-mix(in srgb, var(--qc-btn-tint) 7%, transparent));
}
/* The other two semantic outlines are Tabler's, and only their label is ours:
   the fill colour as text was 2.3:1 (success) and 1.5:1 (warning) on a card. */
.btn-outline-success {
  --tblr-btn-color: var(--qc-success-text);
  --tblr-btn-border-color: color-mix(in srgb, var(--qc-success-text) 40%, transparent);
}
.btn-outline-warning {
  --tblr-btn-color: var(--qc-warning-text);
  --tblr-btn-border-color: color-mix(in srgb, var(--qc-warning-text) 40%, transparent);
}
.btn-secondary:hover,
.btn-outline-primary:hover,
.btn-outline-secondary:hover,
.btn-info:hover {
  color: var(--qc-primary-ink);
  border-color: color-mix(in srgb, var(--qc-btn-tint) 48%, transparent);
  background: color-mix(in srgb, var(--qc-btn-tint) 16%, transparent);
}

/* Disabled: the colour goes, not the legibility. A well, the tertiary ink, a
   rule for an edge — never `opacity` on the text, and no longer a dulled green,
   which read as a button of another kind rather than one that is off. */
.btn:disabled,
.btn.disabled {
  opacity: 1;
  color: var(--qc-text-tertiary);
  background: var(--qc-surface-well);
  border-color: var(--qc-divider);
  box-shadow: none;
}

/* Inline `.btn-link` actions (table "Modifica", edit icons, …) use the brand
   link color, not Tabler's default blue. Plain `color` (no !important) so a
   `.btn-link.text-danger` delete link keeps its AA-safe red. */
.btn-link { color: var(--qc-primary); }
.btn-link:hover { color: var(--qc-primary-dark); }
[data-bs-theme="dark"] .btn-link:hover { color: var(--qc-primary-light); }

/* Toolbar row — actions and filter fields standing side by side (a page
   header, a filter bar). Buttons are sized by their padding and fields by their
   content, so their natural heights never match. Rather than guess a height
   that fits both, the row stretches every control to the tallest one: whatever
   that turns out to be, the tops and bottoms line up. --qc-control-h is only
   the floor, for a row that holds nothing but buttons.
   Stretching has to be handed down explicitly through each wrapper — a
   stretched .input-icon is of no use if the Tom Select inside it keeps its own
   height. */
.qc-toolbar { align-items: stretch; min-width: 0; }
.qc-toolbar > .btn {
  min-height: var(--qc-control-h);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* The field also keeps the row on one line by giving way itself: it starts at
   its preferred width and shrinks (never past a still-usable minimum) instead
   of pushing the buttons onto a second line. `min-width: 0` is what lets a flex
   item shrink below its content at all. */
.qc-toolbar__field {
  display: flex;
  flex-direction: column;
  flex: 0 1 300px;
  min-width: 11rem;
}
.qc-toolbar__field > .form-control,
.qc-toolbar__field > .ts-wrapper {
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
}
/* The height and the centring are `.ts-control`'s own now; what is left here
   is only the stretching a toolbar row needs. */
.qc-toolbar__field > .ts-wrapper { display: flex; }
.qc-toolbar__field > .ts-wrapper > .ts-control { flex: 1 1 auto; }
@media (min-width: 768px) {
  .qc-toolbar { flex-wrap: nowrap; }
  .qc-toolbar > .btn { white-space: nowrap; }
}
/* Phones: one control per line beats three squeezed ones. */
@media (max-width: 767.98px) {
  .qc-toolbar__field { flex: 1 1 100%; }
}

/* Uploaded-file card — a stored document (the estimate model in the system
   settings) shown with its name, its weight and a way to get it back. */
.qc-filecard {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: .7rem .9rem;
  border: 1px solid var(--qc-border); border-radius: var(--qc-radius-card);
  background: var(--qc-surface-light);
}
.qc-filecard__icon {
  display: grid; place-items: center;
  width: 36px; height: 36px; flex: 0 0 36px; border-radius: 10px;
  color: var(--qc-primary);
  background: var(--qc-layer-wash);
}
.qc-filecard__meta { display: flex; flex-direction: column; min-width: 0; }
.qc-filecard__meta b { color: var(--qc-text-primary); overflow-wrap: anywhere; }
.qc-filecard__meta span { font-size: var(--qc-fs-sm); color: var(--qc-text-secondary); }
/* Only the first action is pushed to the far side; the ones after it stay next
   to it. With `auto` on every button the free space would be split between
   them, leaving the pair drifting apart. */
.qc-filecard .btn { margin-left: auto; }
.qc-filecard .btn ~ .btn { margin-left: 0; }

/* Placeholder reference for the estimate Word model */
.qc-tokens > summary {
  cursor: pointer; font-weight: 600; color: var(--qc-text-secondary);
}
.qc-tokens > summary:hover { color: var(--qc-primary); }
.qc-tokens p { font-size: var(--qc-fs-md); color: var(--qc-text-secondary); }
.qc-tokens__list { display: flex; flex-wrap: wrap; gap: .35rem; }
.qc-tokens__list code {
  padding: .1rem .45rem; border-radius: var(--qc-radius-sm);
  font-size: var(--qc-fs-sm); color: var(--qc-primary);
  background: var(--qc-layer-selected);
  border: 1px solid color-mix(in srgb, var(--qc-primary) 25%, transparent);
}

/* ================================================================== *
 * §5.2 — Circular icon button (glass)
 * ================================================================== */
.qc-icon-btn {
  width: 44px; height: 44px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--qc-primary);
  background: var(--qc-layer-selected);
  border: 1.2px solid color-mix(in srgb, var(--qc-primary) 35%, transparent);
  box-shadow: 0 2px 12px color-mix(in srgb, var(--qc-primary) 18%, transparent);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  transition: transform var(--qc-dur-press) var(--qc-ease-press),
              background-color var(--qc-dur-fast) var(--qc-ease-fast);
  cursor: pointer;
}
.qc-icon-btn:hover { background: var(--qc-layer-highlight); }
.qc-icon-btn:active { transform: scale(.88); }
/* The glyph fills the circle: a Tabler webfont `.ti` otherwise inherits the
   bar's 13px text and reads as a dot in a 44px ring. Both spellings — the font
   icon and the inline SVG `.icon` — land at the same size. */
.qc-icon-btn .icon { width: 24px; height: 24px; stroke-width: 1.75; }
.qc-icon-btn > .ti { font-size: 1.5rem; line-height: 1; }

/* ================================================================== *
 * §5.3 — Card (raised: near-white surface + hairline + soft shadow)
 * ================================================================== */
.card {
  background-color: var(--qc-card-bg);
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-card);
  box-shadow: var(--qc-shadow-sm);
  transition: border-color var(--qc-dur-fast) var(--qc-ease-fast),
              box-shadow var(--qc-dur-fast) var(--qc-ease-fast),
              transform var(--qc-dur-fast) var(--qc-ease-fast);
}
/* ---- Arriving ----
   What opens over the page — a menu, a select's list, a suggestion, a toast, a
   tip — appeared all at once. It arrives now: from nothing, a few pixels and
   two hundredths of scale, in the fast duration. Written with `translate` and
   `scale`, never `transform`: Bootstrap's menus are *placed* by Popper with an
   inline `transform`, and animating that property moves them. Entrance only —
   leaving stays immediate, because a menu that lingers after the click is
   worse than one that is gone. A browser without `@starting-style` shows them
   as before, and reduced motion zeroes the duration (tokens.css). Dialogs keep
   Tabler's own animation; the date picker has its own. */
.dropdown-menu,
.ts-dropdown,
.qc-suggest,
.qc-wiz__suggest,
.qc-evcard,
.qc-flagpop,
.qc-tip,
.qc-crumbs__pop,
.qc-toast {
  transition: opacity var(--qc-dur-fast) var(--qc-ease-fast),
              translate var(--qc-dur-fast) var(--qc-ease-fast),
              scale var(--qc-dur-fast) var(--qc-ease-fast);
}
@starting-style {
  .dropdown-menu.show,
  .ts-dropdown,
  .qc-suggest,
  .qc-wiz__suggest,
  .qc-evcard,
  .qc-flagpop,
  .qc-tip,
  .qc-crumbs__pop,
  .qc-toast.show {
    opacity: 0;
    translate: 0 -4px;
    scale: .98;
  }
}

/* ---- Scroll edges ----
   A container with more to one side fades into it instead of being cut clean:
   a clean cut reads as the end. `assets/qc_scroll_fade.js` marks the
   containers (`data-qc-fade` is the axis) and which ends have something beyond
   them; a grid is marked by its own controller and drawn in ag-grid.css.
   A mask, not a painted gradient: it fades the content itself, so it is right
   on a card, in a dialog and on the page alike, with no surface to guess. The
   two lengths are registered so they can be eased; a browser that does not
   register them shows the fade without the easing. `x-end` is a container
   whose near edge is a column that stays put: only the far edge is masked,
   and the column casts a shadow instead (`--qc-scroll-shade`). */
@property --qc-fade-a { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --qc-fade-b { syntax: "<length>"; inherits: false; initial-value: 0px; }
[data-qc-fade]:is([data-qc-fade-start], [data-qc-fade-end]):not(.qc-grid__grid) {
  --qc-fade-to: right;
  -webkit-mask-image: linear-gradient(to var(--qc-fade-to), transparent, #000 var(--qc-fade-a), #000 calc(100% - var(--qc-fade-b)), transparent);
  mask-image: linear-gradient(to var(--qc-fade-to), transparent, #000 var(--qc-fade-a), #000 calc(100% - var(--qc-fade-b)), transparent);
  transition: --qc-fade-a var(--qc-dur-fast) var(--qc-ease-fast),
              --qc-fade-b var(--qc-dur-fast) var(--qc-ease-fast);
}
[data-qc-fade="y"] { --qc-fade-to: bottom; }
[data-qc-fade]:not([data-qc-fade="x-end"])[data-qc-fade-start] { --qc-fade-a: 1.5rem; }
[data-qc-fade][data-qc-fade-end] { --qc-fade-b: 1.5rem; }
[dir="rtl"] [data-qc-fade]:not([data-qc-fade="y"]) { --qc-fade-to: left; }

/* The column that stays put, once something has gone under it. */
.qc-pivot[data-qc-fade-start] .qc-pivot__rowhead,
.qc-planboard__scroll[data-qc-fade-start] .qc-planboard__crew {
  box-shadow: 8px 0 8px -6px var(--qc-scroll-shade);
}
@media (min-width: 768px) {
  .qc-pivot--days[data-qc-fade-start] .qc-pivot__rowhead { box-shadow: none; }
  .qc-pivot--days[data-qc-fade-start] .qc-pivot__table tr > .qc-pivot__total:nth-child(3) {
    box-shadow: 8px 0 8px -6px var(--qc-scroll-shade);
  }
}

/* ---- One band ----
   A line that says something about the page, above or inside it: a list that
   is narrowed, an agreement that has ended, why a field is there, a seat
   count, something to look at. There were eight recipes for it, a few points
   of percentage apart. One: a wash of a hue, an edge of it, the field's
   radius. The hue is primary, or the warning's — and then the wash is
   stronger, because a pale amber has to show on near-white. Each class keeps
   its own layout and ink where it is written. */
.qc-narrowed,
.qc-ended,
.qc-form__explain,
.qc-usernew__seat,
.qc-form__notice,
.qc-docpreview__warning,
.qc-alertbanner {
  --qc-band: var(--qc-primary);
  --qc-band-wash: 8%;
  --qc-band-edge: 30%;
  border-radius: var(--qc-radius-input);
  background: color-mix(in srgb, var(--qc-band) var(--qc-band-wash), transparent);
  border: 1px solid color-mix(in srgb, var(--qc-band) var(--qc-band-edge), transparent);
}
.qc-form__notice,
.qc-usernew__seat.is-over,
.qc-docpreview__warning,
.qc-alertbanner {
  --qc-band: var(--qc-warning);
  --qc-band-wash: 16%;
  --qc-band-edge: 45%;
}

/* Only what can be pressed answers the pointer. Every card used to shift its
   edge towards green on hover, the ones that do nothing included. */
.stat-card[data-stat-filter]:hover {
  border-color: color-mix(in srgb, var(--qc-primary) 30%, var(--qc-border));
}
/* Interactive cards lift on hover */
/* A stat card that is also the way into the list under it: pressing one narrows
   the grid to what it counted, pressing it again goes back to everything. The
   recipe lives here rather than in a <style> on the page that happened to want
   it first, so the customer list and the article list cannot drift apart. The
   pressed state is `aria-pressed`, which is the same fact the screen reader
   reads — never a class that only the eye can see. */
.stat-card[data-stat-filter] { cursor: pointer; }
.stat-card[data-stat-filter]:focus-visible {
  outline: 2px solid var(--qc-primary);
  outline-offset: 2px;
}
.stat-card[data-stat-filter][aria-pressed="true"] {
  border-color: var(--qc-primary);
  box-shadow: inset 0 0 0 1px var(--qc-primary);
}

/* What is being narrowed, in words, with the way out beside it.

   A list can be short for two reasons — there is little of it, or somebody
   asked for a part of it — and the two look identical. So anything that opens
   a list already narrowed says so: a card pressed on arrival because the URL
   asked for it (`?focus=`, assets/qc_focus.js), or a page whose whole query
   was replaced (`/invoice/?state=overdue`). Both come from a row behind the
   bell, and a row that counts four invoices and opens a list of four with no
   explanation is a list somebody will read as "that is all there is".

   Here rather than on the page that wanted it first: the plan board wrote this
   recipe as `.qc-planfilter` and it is `.qc-narrowed` now, so the ledger and
   the register cannot drift away from it.

   Only on screen while something is narrowed — the same reasoning the
   calendar's "Mostra tutto" is drawn by: a control that clears a state nobody
   has set is a control that has to be read and then ignored. */
.qc-narrowed {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .625rem;
  margin-bottom: .75rem;
  padding: .5rem .875rem;
  font-size: var(--qc-fs-md);
  color: var(--qc-text-secondary);
}
.qc-narrowed[hidden] { display: none; }
.qc-narrowed > span { flex: 1 1 auto; min-width: 0; }

/* ---------------------------------------------------------------------------
   Two views of one page — the plan board's squadre / operatori, the
   Pianificazione's lista / mappa. Here rather than on either page because both
   draw it.

   Two links styled as one segmented control. They are links and not buttons
   because each view has an address: `?view=people` can be bookmarked, opened in
   a tab, sent to somebody. What this codebase refuses is a tab that cannot be
   linked to, which is a different thing.
   --------------------------------------------------------------------------- */

.qc-planviews {
  display: inline-flex;
  padding: 2px;
  border-radius: var(--qc-radius-pill, 999px);
  background: var(--qc-surface-well);
  box-shadow: inset 0 0 0 1px var(--qc-divider);
}

.qc-planview {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .3rem .8rem;
  border-radius: var(--qc-radius-pill, 999px);
  font-size: var(--qc-fs-sm);
  font-weight: 600;
  color: var(--qc-text-secondary);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-planview:hover { color: var(--qc-text-primary); }
/* `.qc-planview.is-on` is the segmented control's chosen recipe, written once
   in forms.css. */
@media (prefers-reduced-motion: reduce) {
  .qc-planview { transition: none; }
}

/* A terminated agreement's band (commission/show.html.twig): why it ended,
   and the two things one does next — renew it, or put it away. The
   narrowed band's tint, because it is the same kind of statement: this is
   not the ordinary state of the page. */
.qc-ended {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .625rem 1rem;
  padding: .625rem .875rem;
  font-size: var(--qc-fs-md);
  color: var(--qc-text-secondary);
}
.qc-ended__why { flex: 1 1 16rem; min-width: 0; }
.qc-ended__actions { display: flex; flex-wrap: wrap; gap: .5rem; }

/* "Cosa fare ora" on a new agreement's sheet (commission/_next_steps.html.twig):
   numbered steps down a rail, each with the one or two buttons that do it.
   The green wash and rail of the home's first-steps strip, in a plain card,
   because this is a page's guidance rather than a tile that has to be noticed
   among others. */
.qc-nextsteps {
  border-left: 3px solid var(--qc-primary);
  background: color-mix(in srgb, var(--qc-primary) 5%, var(--qc-card-bg));
}
.qc-nextsteps__title {
  font-family: var(--qc-font-display);
  font-size: var(--qc-fs-xl);
  margin: 0 0 .25rem;
  color: var(--qc-text-primary);
}
.qc-nextsteps__lede { margin: 0 0 1rem; color: var(--qc-text-secondary); font-size: var(--qc-fs-md); }
.qc-nextsteps__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.qc-nextsteps__step { display: flex; gap: .875rem; align-items: flex-start; }
.qc-nextsteps__tick {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--qc-primary) 55%, transparent);
  color: var(--qc-primary);
  font: 600 var(--qc-fs-sm)/1 var(--qc-font-body);
  font-variant-numeric: tabular-nums;
}
.qc-nextsteps__step.is-done .qc-nextsteps__tick {
  background: var(--qc-primary);
  border-color: var(--qc-primary);
  color: var(--qc-on-primary);
}
.qc-nextsteps__body { flex: 1 1 auto; min-width: 0; }
.qc-nextsteps__name { font-size: var(--qc-fs-lg); font-weight: 600; margin: .2rem 0 .125rem; color: var(--qc-text-primary); }
.qc-nextsteps__text { margin: 0 0 .625rem; color: var(--qc-text-secondary); font-size: var(--qc-fs-md); }
.qc-nextsteps__body .qc-estimate-summary { margin-bottom: 0; }
.qc-nextsteps__actions { display: flex; flex-wrap: wrap; gap: .5rem; }

/* An agreement's management as a strip (commission/_timeline.html.twig):
   one segment per job window, placed by percentage, tinted by its stage —
   the same four words as the stat cards — with today as a line. Read-only on
   the sheet and on Lavori; draggable on Modifica finestre (.qc-timeline--edit,
   forms.css). */
.qc-timeline { --qc-tl-h: .75rem; position: relative; }
.qc-timeline__track {
  position: relative;
  height: var(--qc-tl-h);
  border-radius: var(--qc-radius-pill);
  background: color-mix(in srgb, var(--qc-text-secondary) 12%, transparent);
}
.qc-timeline__seg {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: var(--qc-radius-pill);
  box-shadow: 0 0 0 1px var(--qc-card-bg);
  transition: left var(--qc-dur-fast) var(--qc-ease-standard), width var(--qc-dur-fast) var(--qc-ease-standard);
}
.qc-timeline__seg.is-done,     .qc-timeline__dot.is-done     { background: var(--qc-status-green); }
.qc-timeline__seg.is-scheduled,.qc-timeline__dot.is-scheduled{ background: var(--qc-status-lime); }
.qc-timeline__seg.is-to_plan,  .qc-timeline__dot.is-to_plan  { background: color-mix(in srgb, var(--qc-status-yellow) 70%, transparent); }
.qc-timeline__seg.is-late,     .qc-timeline__dot.is-late     { background: var(--qc-status-orange); }
.qc-timeline__today {
  position: absolute;
  top: -.375rem;
  bottom: -.375rem;
  width: 2px;
  margin-left: -1px;
  background: var(--qc-text-primary);
  border-radius: 1px;
  pointer-events: none;
}
.qc-timeline__todaylabel {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  padding: 0 .25rem;
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  color: var(--qc-text-primary);
  white-space: nowrap;
}
.qc-timeline__today.is-start .qc-timeline__todaylabel { left: 0; transform: none; padding-left: 0; }
.qc-timeline__today.is-end .qc-timeline__todaylabel { left: auto; right: 0; transform: none; padding-right: 0; }
.qc-timeline__ends {
  display: flex;
  justify-content: space-between;
  margin-top: .375rem;
  font-size: var(--qc-fs-xs);
  color: var(--qc-text-secondary);
}
.qc-timeline__legend {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
  list-style: none;
  margin: .5rem 0 0;
  padding: 0;
  font-size: var(--qc-fs-xs);
  color: var(--qc-text-secondary);
}
.qc-timeline__legend li { display: inline-flex; align-items: center; gap: .375rem; }
.qc-timeline__dot { width: .625rem; height: .625rem; border-radius: 50%; }
@media (prefers-reduced-motion: reduce) { .qc-timeline__seg { transition: none; } }

/* Progress as a ring: a conic gradient with the figure in its hole. */
.qc-ring {
  --qc-ring-pct: 0;
  --qc-ring-size: 4rem;
  position: relative;
  flex: 0 0 auto;
  width: var(--qc-ring-size);
  height: var(--qc-ring-size);
  border-radius: 50%;
  background: conic-gradient(var(--qc-primary) calc(var(--qc-ring-pct) * 1%), color-mix(in srgb, var(--qc-text-secondary) 16%, transparent) 0);
  display: grid;
  place-items: center;
}
.qc-ring::before {
  content: "";
  position: absolute;
  inset: .4375rem;
  border-radius: 50%;
  background: var(--qc-card-bg);
}
.qc-ring__value {
  position: relative;
  font-family: var(--qc-font-display);
  font-weight: 700;
  font-size: var(--qc-fs-lg);
  color: var(--qc-text-primary);
}
.qc-ring--sm { --qc-ring-size: 3rem; }
.qc-ring--sm::before { inset: .375rem; }
.qc-ring--sm .qc-ring__value { font-size: var(--qc-fs-xs); }

/* The agreement sheet's facts as tiles: an icon, a label, the answer. */
.qc-facttiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: .75rem;
  margin: 0;
}
.qc-facttile {
  display: flex;
  gap: .75rem;
  align-items: flex-start;
  padding: .75rem .875rem;
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-card);
  background: color-mix(in srgb, var(--qc-primary) 3%, var(--qc-card-bg));
  min-width: 0;
}
.qc-facttile__icon {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border-radius: var(--qc-radius-sm);
  background: var(--qc-layer-selected);
  color: var(--qc-primary);
}
.qc-facttile dt { font-size: var(--qc-fs-xs); font-weight: 500; color: var(--qc-text-secondary); margin-bottom: .125rem; }
.qc-facttile dd { margin: 0; font-weight: 600; color: var(--qc-text-primary); min-width: 0; }
.qc-facttile__sub { display: block; font-weight: 400; font-size: var(--qc-fs-sm); color: var(--qc-text-secondary); }

/* The agreement sheet's hero and its progress row. */
.qc-comhero { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: .75rem 1rem; }
.qc-comhero__name { font-family: var(--qc-font-display); font-size: var(--qc-fs-2xl); font-weight: 700; line-height: 1.2; margin: 0; color: var(--qc-text-primary); }
.qc-comhero__where { display: flex; align-items: center; gap: .375rem; margin-top: .25rem; color: var(--qc-text-secondary); font-size: var(--qc-fs-lg); }
.qc-comprogress { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1.25rem; }
.qc-comprogress__text { display: flex; flex-direction: column; gap: .375rem; }
.qc-comprogress__main { font-weight: 600; color: var(--qc-text-primary); }
.qc-minw0 { min-width: 0; }
/* A module still being proven: one word after the entry's title, outlined
   and quiet so it reads as a note rather than a figure asking for somebody.
   It sits against the title, and the caret keeps the row's far end. */
.navbar-vertical .navbar-nav .nav-link > .qc-nav-beta {
  flex: 0 0 auto;
  margin-left: .5rem;
  padding: 0 .375rem;
  border: 1px solid var(--qc-field-border);
  border-radius: var(--qc-radius-pill);
  color: var(--qc-text-secondary);
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  line-height: 1.125rem;
  white-space: nowrap;
}
@media (min-width: 992px) {
  /* The rail has room for icons only; the word goes with the title. */
  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .navbar-nav .nav-link > .qc-nav-beta {
    display: none;
  }
}
/* The rail's "something waits here" mark (components/_side_nav, `alert`). */
.qc-sidenav__alert {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--qc-status-orange) 18%, transparent);
  color: var(--qc-status-orange);
  font-size: var(--qc-fs-md);
}
.qc-addmenu { min-width: 17rem; }
.qc-addmenu__item { display: flex; align-items: flex-start; gap: .625rem; white-space: normal; padding-top: .5rem; padding-bottom: .5rem; }
.qc-addmenu__item > .ti { margin-top: .125rem; color: var(--qc-primary); }
.qc-addmenu__lede { display: block; font-size: var(--qc-fs-xs); color: var(--qc-text-secondary); }
.qc-comtimeline { padding-top: .75rem; }
.qc-comnote {
  margin: 0;
  padding: .5rem .875rem;
  border-left: 3px solid color-mix(in srgb, var(--qc-primary) 45%, transparent);
  color: var(--qc-text-secondary);
  font-size: var(--qc-fs-lg);
}

/* "Cosa fare ora" on an agreement's Lavori page: a few lines, each a fact and
   the gesture that deals with it. The next-steps card's accent, lighter. */
.qc-todo { border-left: 3px solid var(--qc-primary); }
.qc-todo__title {
  font-family: var(--qc-font-display);
  font-size: var(--qc-fs-xl);
  margin: 0 0 .5rem;
  color: var(--qc-text-primary);
}
.qc-todo__list { list-style: none; margin: 0; padding: 0; }
.qc-todo__item {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .5rem 0;
  border-top: 1px solid var(--qc-border);
}
.qc-todo__item:first-child { border-top: 0; padding-top: 0; }
.qc-todo__item:last-child { padding-bottom: 0; }
.qc-todo__icon {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--qc-layer-selected);
  color: var(--qc-primary);
}
.qc-todo__text { flex: 1 1 auto; min-width: 0; font-size: var(--qc-fs-md); color: var(--qc-text-primary); }
.qc-todo__action { flex: 0 0 auto; }
@media (max-width: 575.98px) {
  .qc-todo__item { flex-wrap: wrap; }
  .qc-todo__action { flex-basis: 100%; padding-left: 2.5rem; }
}

.card.card-link-pop:hover,
a.card:hover {
  border-color: color-mix(in srgb, var(--qc-primary) 30%, var(--qc-border));
  box-shadow: var(--qc-shadow-md);
  transform: translateY(-2px);
}
/* Dialogs/sheets/dropdowns: topmost surface + full shadow */
/* Bootstrap's veil is a navy at 24%: over a page that is dark already it
   dimmed nothing (1.05:1), and a dialog opened onto a page as bright as itself.
   The side panels' own scrim instead. Dark only — the light veil is fine. */
[data-bs-theme="dark"] .modal-backdrop { --tblr-backdrop-bg: var(--qc-scrim); }
[data-bs-theme="dark"] .modal-backdrop.show { opacity: 1; }
/* A veil over a modal that is already open: two full ones added up and the
   sheet under the question went black. The class is put there by whoever
   opened the second modal (qc_dialog.js, the attachment preview). */
[data-bs-theme="dark"] .modal-backdrop.is-stacked { --tblr-backdrop-bg: var(--qc-scrim-stacked); }

.modal-content,
.dropdown-menu {
  background-color: var(--qc-surface-overlay);
  border: 1px solid var(--qc-edge);
  border-radius: var(--qc-radius-panel);
  box-shadow: var(--qc-shadow-lg), var(--qc-shadow-top);
}

/* The grip a row is carried by. It was a mouse affordance and nothing else,
   aria-hidden, on the reasoning that the order it sets is only a preference;
   it is a <button> now, because the pair of ↑/↓ buttons the statuses page had
   was the keyboard way round and dragging replaced it. Hence the reset of the
   browser's own button chrome. */
.qc-handle {
  display: inline-flex;
  align-items: center;
  padding: .25rem;
  border: 0;
  border-radius: var(--qc-radius-sm);
  background: transparent;
  color: var(--qc-text-tertiary);
  cursor: grab;
  touch-action: none;
}
.qc-handle:active { cursor: grabbing; }
tr:hover > td > .qc-handle { color: var(--qc-text-secondary); }

/* ------------------------------------------------------------------ *
 * The work-statuses table
 *
 * Three things that were left to the content and so moved from row to
 * row: whether a status is retired, the badges that say what it is, and
 * where its buttons start.
 * ------------------------------------------------------------------ */

/* A retired status reads as one at a glance, not only where the badge is.
   On the row and not on the cells: a Tabler cell is transparent and paints
   its hover as an inset shadow, so this shows through and the hover still
   lands on top of it. */
.qc-statusrow--retired {
  background: color-mix(in srgb, var(--qc-text-tertiary) 12%, transparent);
}

/* A column of their own, so the eye scans one place down the table
   instead of hunting along the end of names of every length. */
.qc-statusrow__tags { white-space: nowrap; }
.qc-statusrow__tags .badge + .badge { margin-left: .35rem; }

/* Left, not right: the column shrinks to the widest row, so every
   "Modifica" starts on the same pixel — including on the four statuses
   that have no delete button beside it. */
.qc-rowactions,
.qc-statusrow__actions {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: .5rem;
}
/* The buttons in a row are outlined and small and they carry their word:
   `btn-outline-secondary btn-sm` to edit, `btn-outline-danger btn-sm` to
   delete, each with a `ti` icon and a label — docs/brand/forms.md §6.6. An
   icon on its own is a guess, and a ghost button on a table row is invisible
   until the pointer is already on it. */
.qc-rowactions > form { margin: 0; }

/* While a row is being carried. Sortable puts this on the placeholder. */
.sortable-ghost {
  opacity: 1;                       /* never opacity: the token layer tints instead */
  background: var(--qc-layer-selected);
}

/* ------------------------------------------------------------------ *
 * Tom Select (autocomplete selects) — brand skin for the control, the
 * selected chips and the OPENED dropdown. Only the stock
 * tom-select.bootstrap5.css shipped before, so the popup ignored the
 * tokens (and the dark theme), which is why open selects rendered badly.
 * Tokens flip under [data-bs-theme="dark"], so one rule set covers both.
 * ------------------------------------------------------------------ */
/* The height is the control's own, not the sum of what happens to be in it.
   Under eight options qc_select.js passes `controlInput: null`, and Tom Select
   then creates its input and never appends it — so a select with nothing
   chosen has no child at all and collapses to twice its padding, about 13px.
   It reads as a rendering failure, and it happens on every unfilled short
   select, not only on the empty ones. */
.ts-control {
  background-color: var(--qc-field-bg) !important;
  border: 1px solid var(--qc-field-border) !important;
  border-radius: var(--qc-radius-input) !important;
  color: var(--qc-text-primary) !important;
  padding: .35rem .5rem !important;
  box-shadow: none !important;
  gap: .3rem;
  min-height: var(--qc-control-h);
  align-items: center;
}

/* The same absent input is where Tom Select writes the placeholder, so the
   instruction never reaches the screen either. qc_select.js copies it onto the
   control and it is drawn here — `has-items` is Tom Select's own class for
   "something is chosen", so the text disappears the moment one is. */
.ts-wrapper:not(.has-items) .ts-control::before {
  content: attr(data-qc-placeholder);
  color: var(--qc-text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Nothing to choose from is a state of its own, and saying so is the whole
   point: a control that looks ready but opens on nothing is worse than one
   that admits it is empty. It stays focusable — the field is still part of the
   form and still has to be reachable (docs/brand/forms.md §8). */
.qc-select--empty .ts-control {
  background-color: var(--qc-surface-light) !important;
  cursor: not-allowed;
}
.qc-select--empty .ts-control::before { font-style: italic; }
.ts-control input,
.ts-wrapper.single .ts-control { color: var(--qc-text-primary) !important; }
.ts-control input::placeholder { color: var(--qc-text-tertiary) !important; }
.ts-wrapper.focus .ts-control,
.ts-control:focus-within {
  border-color: var(--qc-primary) !important;
  box-shadow: var(--qc-focus-ring) !important;
}
.ts-wrapper.disabled .ts-control { background-color: var(--qc-surface-well) !important; border-style: dashed !important; opacity: 1; }
.ts-wrapper.disabled .ts-control,
.ts-wrapper.disabled .ts-control input { color: var(--qc-text-tertiary) !important; }

/* One value is text, never a chip (docs/brand/forms.md §3). A chip says "one of
   several, removable"; on a lone customer it read as a tag, and in a document's
   88px unit cell "a corpo" wrapped inside its pill and pushed the row taller. */
.ts-wrapper.single .ts-control { flex-wrap: nowrap; }
.ts-wrapper.single .ts-control > .item {
  display: block;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--qc-text-primary);
}
/* A remote field still types inside the control: while it does, the search
   takes the value's place instead of sitting beside it. */
.ts-wrapper.single.input-active:not(.plugin-dropdown_input) .ts-control > .item { display: none; }
/* The placeholder dropdown_input leaves in the control: the field's own words,
   in the field's own ink. */
.ts-control > .items-placeholder {
  padding: 0; background: transparent; color: var(--qc-text-primary);
  font: inherit; min-width: 0; flex: 1 1 auto;
}
.ts-control > .items-placeholder::placeholder { color: var(--qc-text-tertiary); }

/* The chosen value of a list whose options say more: the name, then its pill. */
.ts-wrapper.single .ts-control > .item.qc-selitem { display: flex; align-items: center; gap: .5rem; flex: 1 1 auto; }
.qc-selitem__text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.qc-selitem .qc-stpill { flex: none; margin-left: auto; }

/* selected values of a multiple select → removable brand chips (green tint).
   The label is centred in the pill on both axes. That needs help horizontally:
   the remove button sits inside the pill, and these labels are short codes, so
   the × took up nearly as much room as the text and pushed it into the left
   half. A spacer mirroring the button's width balances it — text centred,
   × still on the right. Chips without a remove button get no spacer and are
   centred by their own symmetric padding. */
.ts-wrapper.multi .ts-control .item {
  --qc-chip-remove-w: 1.15rem;
  display: inline-flex; align-items: center; justify-content: center; gap: .2rem;
  background: color-mix(in srgb, var(--qc-primary) 14%, var(--qc-card-bg)) !important;
  color: color-mix(in srgb, var(--qc-primary) 55%, var(--qc-text-primary)) !important;
  border: 1px solid color-mix(in srgb, var(--qc-primary) 30%, transparent) !important;
  border-radius: var(--qc-radius-pill) !important;
  padding: .12rem .35rem !important;
  font-weight: 600; font-size: var(--qc-fs-sm);
  line-height: 1.4;
  text-align: center;
}
.ts-control .item:has(.remove)::before {
  content: "";
  flex: none;
  width: var(--qc-chip-remove-w);
}
.ts-wrapper.multi .ts-control .item.active {
  background: color-mix(in srgb, var(--qc-primary) 22%, var(--qc-card-bg)) !important;
  border-color: color-mix(in srgb, var(--qc-primary) 45%, transparent) !important;
}
/* A value that is a service or a machine (`data-qc-type-pill`, qc_select.js)
   wears its own chip: the green one around it gives way, the × stays beside. */
.ts-wrapper.multi .ts-control .item:has(> .tpill) {
  background: none !important;
  border-color: transparent !important;
  padding: 0 !important;
}
.ts-control .item:has(> .tpill)::before { content: none; }
/* Fixed width, so the ::before spacer above can mirror it exactly. */
.ts-control .item .remove {
  border: 0 !important; color: inherit !important;
  width: var(--qc-chip-remove-w); flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 !important;
  border-radius: var(--qc-radius-pill); opacity: .65;
  transition: opacity var(--qc-dur-fast) var(--qc-ease-fast),
              background-color var(--qc-dur-fast) var(--qc-ease-fast);
}
.ts-control .item .remove:hover {
  opacity: 1;
  background: color-mix(in srgb, var(--qc-primary) 24%, transparent) !important;
}

/* opened dropdown → elevated brand panel */
/* The field's radius and a step more: the panel's 20px on a menu hanging off a
   10px field read as a bubble, and in a narrow cell as a pill. */
.ts-dropdown {
  background: var(--qc-surface-overlay) !important;
  border: 1px solid var(--qc-edge) !important;
  border-radius: calc(var(--qc-radius-input) + 4px) !important;
  box-shadow: var(--qc-shadow-lg), var(--qc-shadow-top) !important;
  color: var(--qc-text-primary) !important;
  margin-top: .3rem;
  padding: .25rem;
  overflow: hidden;
}
.ts-dropdown .option {
  border-radius: var(--qc-radius-btn);
  padding: .45rem .6rem;
  color: var(--qc-text-primary);
}
.ts-dropdown .option:hover,
.ts-dropdown .active {
  background: var(--qc-layer-selected) !important;
  color: var(--qc-text-primary) !important;
}
.ts-dropdown .selected {
  background: var(--qc-layer-highlight) !important;
}
/* What the search found is marked in the brand's ink. Tom Select's own mark is
   a yellow wash (tom-select.bootstrap5.css, loaded after this sheet — hence
   the doubled class), outside the palette and the same in the dark theme. */
.ts-dropdown.ts-dropdown [data-selectable] .highlight {
  background: none;
  color: inherit;
  font-weight: 700;
  text-decoration: underline 2px color-mix(in srgb, var(--qc-primary) 60%, transparent);
  text-underline-offset: 2px;
  border-radius: 0;
}

/* The search, inside the menu (dropdown_input, from eight options up — see
   assets/qc_select.js): a field set into the panel, so the control above it
   keeps one height and one look whatever the length of the list. */
.ts-dropdown .dropdown-input-wrap {
  position: relative;
  margin: .1rem .1rem .3rem;
}
.ts-dropdown .dropdown-input-wrap::before {
  content: "";
  position: absolute; left: .6rem; top: 50%;
  width: 14px; height: 14px; margin-top: -7px;
  background: var(--qc-text-tertiary);
  -webkit-mask: var(--qc-select-lens) center / contain no-repeat;
          mask: var(--qc-select-lens) center / contain no-repeat;
  pointer-events: none;
}
.ts-dropdown .dropdown-input-wrap .dropdown-input {
  display: block;
  width: 100%;
  min-height: 2.25rem;
  padding: .35rem .6rem .35rem 1.95rem;
  background: var(--qc-field-bg);
  border: 1px solid var(--qc-field-border);
  border-radius: var(--qc-radius-input);
  color: var(--qc-text-primary);
  font: inherit;
  box-shadow: none;
  outline: none;
}
.ts-dropdown .dropdown-input-wrap .dropdown-input::placeholder { color: var(--qc-text-tertiary); }
.ts-dropdown .dropdown-input-wrap .dropdown-input:focus { border-color: var(--qc-primary); }
:root { --qc-select-lens: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E"); }

/* The menu of a document's cell (unit, rate): as wide as its options, not as
   the 88px it hangs from. Tom Select writes the cell's width inline; a
   min-width outranks it. */
.ts-dropdown.qc-select-menu--compact { min-width: 13rem; }

/* An option that says more than its name — a customer's place and type, a
   unit's full name (`data-qc-lede`, `data-qc-pill`, read by qc_select.js). */
.qc-selopt {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: .6rem;
  align-items: center;
}
.qc-selopt__text,
.qc-selopt__lede { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.qc-selopt__lede { grid-column: 1; font-size: var(--qc-fs-xs); color: var(--qc-text-secondary); }
.qc-selopt .qc-stpill { grid-column: 2; grid-row: 1 / span 2; }
/* In a cell's menu the abbreviation and its name are one line. */
.qc-selopt--inline { display: flex; align-items: baseline; gap: .6rem; }
.qc-selopt--inline .qc-selopt__text { flex: 0 0 3.25rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.qc-selopt--inline .qc-selopt__lede { font-size: var(--qc-fs-sm); }
/* Five two-line options, then scroll — the cap below is for one line. */
.ts-dropdown:has(.qc-selopt:not(.qc-selopt--inline)) .ts-dropdown-content { max-height: calc(3.05rem * 5); }

/* The clear button is Tom Select's to show on hover; in a document's cell it
   would sit on the value — "none" is an option of the list there instead. */
.qc-wiz__row .ts-wrapper .clear-button { display: none; }
/* …and the room tom-select.bootstrap5.css keeps for it (an !important padding
   built from these two) goes with it: in a 76px cell it left the value 16px.
   Doubled class: that sheet loads after this one and sets the caret's share
   on `.ts-wrapper.single`. */
.qc-wiz__row .ts-wrapper.ts-wrapper { --ts-pr-clear-button: 0px; --ts-pr-caret: 22px; }

.ts-dropdown .no-results,
.ts-dropdown .optgroup-header,
.ts-dropdown .create { color: var(--qc-text-tertiary); padding: .4rem .6rem; font-size: var(--qc-fs-sm); }
.ts-dropdown .create { color: var(--qc-primary); }
/* Five options, then scroll. The option box is its own line height plus the
   .45rem padding above and below set on .ts-dropdown .option — keep the two in
   step if either changes. */
.ts-dropdown { --qc-ts-option-h: 2.3rem; }
.ts-dropdown-content {
  max-height: calc(var(--qc-ts-option-h) * 5);
  overflow-y: auto;
}
/* Parented to <body> (see qc_select.js), so it has to clear the sticky page
   chrome and any open modal (1055) rather than just its own stacking context.
   Doubled class, because tom-select.bootstrap5.css is loaded after this sheet
   and its own `.ts-dropdown { z-index: 1000 }` won at equal specificity: every
   select inside a modal opened its list underneath it, and read as dead —
   the price list's "Nuova voce" unit was the one reported. */
.ts-dropdown.ts-dropdown { z-index: 1080; }

/* Login language selector — compact, right-aligned combobox (not full width).
   Single-value locale, so drop the removable-chip look for a plain inline label. */
/* One width for every locale. The endonyms are not the same length ("Polski"
   against "Español"), and Tom Select's control grows again by the width of the
   search input it opens on focus — so with `width: auto` the field moved twice:
   once when picked, once when touched. Pinned instead, and the label clipped if
   it ever outgrows it. */
.qc-locale .ts-wrapper { width: 11rem; min-width: 11rem; max-width: 11rem; flex: 0 0 auto; }
.qc-locale .ts-control { flex-wrap: nowrap; overflow: hidden; }
/* Tom Select sizes this input inline as the user types; let it be clipped by the
   control rather than push the control wider. */
.qc-locale .ts-control > input { min-width: 0 !important; flex: 1 1 auto; }
.qc-locale .ts-control .item {
  background: transparent !important;
  border: 0 !important;
  padding: 0 !important;
  color: var(--qc-text-primary) !important;
  max-width: 100%;
  min-width: 0;
}

/* The flag-and-name row, drawn for the selected value and for every option.
   Deliberately not scoped under .qc-locale: app.js parents the dropdown to
   <body>, so by the time an option is drawn it is no longer inside the
   wrapper and a descendant selector would miss it. */
.qc-locale__option {
  display: flex;
  align-items: center;
  gap: .45rem;
  min-width: 0;
}
.qc-locale__option span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.qc-locale__flag {
  flex: 0 0 auto;
  width: 1.25rem;
  height: .875rem;
  border-radius: 2px;
  object-fit: cover;
  /* A hairline so a flag with a white band keeps an edge against the field. */
  border: 1px solid var(--qc-border);
}
/* Pre-enhancement width: the native <select> defaults to form-select's
   width:100% and would stretch across the flex row for one frame before Tom
   Select shrinks it. Pin it to the ts-wrapper width so there is no layout shift. */
.qc-locale__select { width: 11rem; flex: 0 0 auto; }

/* ------------------------------------------------------------------ *
 * Quill — never show the floating widgets that appeared on the left
 * when clicking inside the editor: the snow `.ql-tooltip` (link popup)
 * and the NodeMover `.ql-node-mover-toolbar` (block drag/move/delete
 * overlay). Both are also disabled server-side (CustomQuillType); this
 * is belt-and-braces so neither can appear, on desktop or mobile.
 * ------------------------------------------------------------------ */
.ql-tooltip,
.ql-bubble .ql-tooltip,
.ql-inline-toolbar,
[class*="ql-node-mover"]:not(.ql-node-mover-styles) { display: none !important; }

/* ================================================================== *
 * §5.5 / §2.3 — Status chips / badges (text chosen by luminance)
 * Status colors = Work constants (yellow/orange/purple/red/lime/cyan).
 * ================================================================== */
/* Tabler 1.x resolves a badge's text through `--tblr-badge-color`, where beta19
   simply used white. Our token layer remaps the body text colour, and the new
   variable inherits from it — so on the dark theme every pill we had not named
   below came out grey-on-green, near invisible. The variable is given a real
   default here; the rules under it stay what they always were, a choice made by
   the luminance of the background. The colour of a status pill is data (see the
   `*_COLOR` constants on Work), so any Tabler hue can turn up: the default has
   to hold on its own, not rely on the list being complete. */
.badge {
  border-radius: var(--qc-radius-pill);
  font-weight: 600;
  --tblr-badge-color: #fff;
}
/* Light grounds: near-black text. Green joins them on the arithmetic — white on
   #66BB6A is 2.2:1, which the guideline's 4.5:1 floor rules out. */
.badge.bg-yellow,
.badge.bg-orange,
.badge.bg-purple,
.badge.bg-lime,
.badge.bg-cyan,
.badge.bg-green,
.badge.bg-success,
.badge.bg-red,
.badge.bg-teal,
.badge.bg-pink,
.badge.bg-info,
.badge.bg-warning,
.badge.bg-danger { color: rgba(0, 0, 0, .87) !important; }

/* The icon well of a stat card — `bg-{colour} text-white avatar`, with the
   colour handed in by the page. It was a filled square with a white icon
   (1.4:1 on lime, 1.7 on the dark theme's mint). It is a well of the hue now:
   a wash of it, the icon in the ink drawn from it (`--st-ink`, further down),
   a thin ring — the state pill's recipe, so a figure above a list and a state
   in its rows speak one language. The class says which hue; both utilities
   are `!important`, hence these. */
.avatar.bg-yellow { --st: var(--qc-status-yellow); }
.avatar.bg-orange { --st: var(--qc-status-orange); }
.avatar.bg-red { --st: var(--qc-status-red); }
.avatar.bg-pink { --st: var(--qc-status-pink); }
.avatar.bg-purple { --st: var(--qc-status-purple); }
.avatar.bg-cyan { --st: var(--qc-status-cyan); }
.avatar.bg-teal { --st: var(--qc-status-teal); }
.avatar.bg-green { --st: var(--qc-status-green); }
.avatar.bg-lime { --st: var(--qc-status-lime); }
.avatar.bg-success { --st: var(--qc-success); }
.avatar.bg-danger { --st: var(--qc-error); }
.avatar.bg-warning { --st: var(--qc-warning); }
.avatar.bg-info { --st: var(--qc-info); }
.avatar.bg-primary,
.avatar.bg-blue,
.avatar.bg-azure,
.avatar.bg-indigo { --st: var(--qc-primary); }
.avatar.bg-secondary,
.avatar.bg-muted,
.avatar.bg-dark { --st: var(--qc-text-tertiary); }
.avatar[class*="bg-"].text-white {
  background-color: color-mix(in srgb, var(--st, var(--qc-primary)) var(--qc-status-wash), transparent) !important;
  color: var(--st-ink) !important;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--st-ink) 22%, transparent);
}
/* A green chip is a lighter green than a green button: the button darkens its
   tint in a gradient and prints white on it, a chip prints near-black. Both are
   the same green a step apart, not two greens. */
.badge.bg-success,
.badge.bg-green { background-color: var(--qc-success-light) !important; }
/* `.bg-primary` paints the background only, leaving the badge's stock white
   text — fine on the light theme's forest green, unreadable on the dark
   theme's mint. Take the text from the token that already tracks it. The blue
   family lands here too: the token layer remaps it onto the brand green. */
.badge.bg-primary,
.badge.bg-blue,
.badge.bg-azure,
.badge.bg-indigo { color: var(--qc-on-primary) !important; }
/* `bg-secondary` is the muted text colour, which flips from dark to light with
   the theme — so the text on it has to flip the other way. `--qc-surface` does
   exactly that, and needs no second rule for the dark theme. */
.badge.bg-secondary,
.badge.bg-dark { color: var(--qc-background) !important; }

/* Blue-family light tints (`bg-azure-lt`/`bg-blue-lt`/`bg-indigo-lt`) pull their
 * background from Tabler's un-remapped `--tblr-*-lt` blue. Re-point to a faint
 * brand-green wash so tinted rows (e.g. every /work/plan row) match the canvas.
 * Green/yellow/orange/red `-lt` are status tints and are left on-brand. */
.bg-azure-lt,
.bg-blue-lt,
.bg-indigo-lt {
  background-color: color-mix(in srgb, var(--qc-primary) 9%, var(--qc-card-bg)) !important;
}

/* The words on a light tint (`bg-*-lt`) — every colour, one rule.
 * Tabler sets them in the hue of their own wash: yellow on pale yellow, orange
 * on pale orange, lime on pale lime, none of which is 4.5:1. The ink is the
 * page's own, tinged with the hue — the `.tpill` treatment, pushed further
 * toward the ink because yellow and lime have no dark of their own. It needs
 * no second rule for the dark theme: `--qc-text-primary` flips with it, and a
 * light ink tinged with the hue is what reads on a dark wash. */
.bg-yellow-lt  { --qc-lt-hue: var(--qc-status-yellow); }
.bg-orange-lt  { --qc-lt-hue: var(--qc-status-orange); }
.bg-red-lt     { --qc-lt-hue: var(--qc-status-red); }
.bg-purple-lt  { --qc-lt-hue: var(--qc-status-purple); }
.bg-pink-lt    { --qc-lt-hue: var(--qc-status-pink); }
.bg-lime-lt    { --qc-lt-hue: var(--qc-status-lime); }
.bg-green-lt   { --qc-lt-hue: var(--qc-status-green); }
.bg-teal-lt    { --qc-lt-hue: var(--qc-status-teal); }
.bg-cyan-lt    { --qc-lt-hue: var(--qc-status-cyan); }
.bg-success-lt { --qc-lt-hue: var(--qc-success); }
.bg-warning-lt { --qc-lt-hue: var(--qc-warning); }
.bg-danger-lt  { --qc-lt-hue: var(--qc-error); }
.bg-info-lt    { --qc-lt-hue: var(--qc-info); }
.bg-secondary-lt { --qc-lt-hue: var(--qc-text-secondary); }
.bg-primary-lt,
.bg-azure-lt,
.bg-blue-lt,
.bg-indigo-lt  { --qc-lt-hue: var(--qc-primary); }
.bg-yellow-lt, .bg-orange-lt, .bg-red-lt, .bg-purple-lt, .bg-pink-lt,
.bg-lime-lt, .bg-green-lt, .bg-teal-lt, .bg-cyan-lt,
.bg-success-lt, .bg-warning-lt, .bg-danger-lt, .bg-info-lt,
.bg-secondary-lt, .bg-primary-lt, .bg-azure-lt, .bg-blue-lt, .bg-indigo-lt {
  color: color-mix(in srgb, var(--qc-lt-hue) 35%, var(--qc-text-primary)) !important;
}

/* Inline `<code>` — Tabler's default chip is a navy blue in dark. Re-tint to a
 * neutral brand surface with legible text (used in help text, e.g. feedback). */
code:not([class]) {
  background: color-mix(in srgb, var(--qc-primary) 8%, var(--qc-surface-light));
  color: var(--qc-text-primary);
  border-radius: var(--qc-radius-sm);
  padding: .1rem .35rem;
}

/* Colored DATA TEXT (numbers, deltas) → AA-safe on-surface variants.
 * Scoped away from `.badge` so status chips keep their on-fill text color.
 * The status hue stays vivid on fills; only plain colored text is retuned. */
.text-warning:not(.badge) { color: var(--qc-warning-text) !important; }
.text-danger:not(.badge)  { color: var(--qc-error-text) !important; }
.text-success:not(.badge) { color: var(--qc-success-text) !important; }

/* ================================================================== *
 * Tables — tabular figures + highlighted row on surface-light
 * ================================================================== */
.table {
  --tblr-table-hover-bg: color-mix(in srgb, var(--qc-primary) 7%, transparent);
  --tblr-table-striped-bg: color-mix(in srgb, var(--qc-surface-light) 55%, transparent);
}
.table td, .table th { border-color: var(--qc-divider); }
/* A header is not a band: the labels in the tertiary ink on the card's own
   surface, and a rule under them a step stronger than the one between rows —
   the grids' header, for the tables that are not grids. */
.table thead th {
  background: transparent;
  color: var(--qc-text-tertiary);
  border-bottom-color: var(--qc-border);
}
/* times/hours/amounts: apply .qc-numeric to numeric columns */
.table .qc-numeric,
td.qc-numeric,
th.qc-numeric { font-variant-numeric: tabular-nums; }

/* ================================================================== *
 * §5.4 — Navigation (a bar on the page's own ground + accent marker)
 * (markup changes live in menu_vertical/base — styling here)
 * ================================================================== */
/* The page's ground carries one thing: a faint light from above, behind the
   content. On the wrapper and not on <body>, so the sign-in pages, which have
   a panel of their own, do not get it. */
.page-wrapper {
  background-image: var(--qc-page-glow);
  background-repeat: no-repeat;
}

/* The frame steps back: the bar stands on the page's ground, and a card is the
   only thing on a screen that is raised. It was the card's colour, so the menu,
   the top bar and every card sat on one plane. Opaque still — the collapsed
   rail opens over the page when pointed at. */
.navbar-vertical {
  background-color: var(--qc-surface-sunken);
  border-right: 1px solid var(--qc-divider);
  /* Tabler draws a 3px green bar down the bar's own edge beside the active
     item. The lifted item and its dot already say where you are. */
  --tblr-navbar-active-border-color: transparent;
}
/* Tabler keeps a scrollbar permanently on the sidebar (overflow-y: scroll),
   which its inline-expanding submenus needed: opening a couple of sections made
   the column taller than the screen. Ours open as flyouts beside the item, so
   the column is a short fixed list — scroll only in the rare case it does not
   fit, and no permanent track eating a strip of the menu. */
.navbar-vertical.navbar-expand-lg { overflow-y: auto; }

/* ---- Width, and the room the page keeps clear for it ----
   Tabler spells 15rem out twice as a literal: the column itself, and the
   `~ .page-wrapper` sibling rule that offsets the page. (Its other offset rule,
   `~ .page`, never applies here — .page is the parent of both, not a sibling.)
   Both are restated against the token so that collapsing moves the bar and the
   page together. Same selectors as Tabler's, which is enough: this sheet is
   loaded after it, and at equal specificity the last one wins.

   The width transition replaces Tabler's `transition: transform .3s`; the
   duration and easing are the brand's §7 tokens. prefers-reduced-motion is
   handled globally in tokens.css. */
@media (min-width: 992px) {
  .navbar-vertical.navbar-expand-lg {
    width: var(--qc-sidebar-w);
    /* The rail clips the labels rather than wrapping them; without this the
       titles would push a horizontal scrollbar into the bar. */
    overflow-x: hidden;
    transition: width var(--qc-dur-standard) var(--qc-ease-standard),
                box-shadow var(--qc-dur-standard) var(--qc-ease-standard);
  }
  /* The room the page keeps clear is stated twice, and the second is not a
     duplicate of the first.

     Tabler's spelling is a sibling rule — `~ .page-wrapper` — which is a rule
     about the bar *being there*, and on every Turbo visit it briefly is not.
     The new body arrives with `<meta name="turbo-permanent-placeholder">`
     standing where the permanent sidebar goes; Turbo makes that body the
     document's and only then puts the real bar back. Any style read taken in
     between — a Stimulus controller measuring itself as it connects, an inline
     script reading a rect — resolves this margin against a `.page` whose first
     child is a `<meta>`, i.e. to 0. The bar arriving a tick later then moves
     the margin back, and with a transition on it that is an animation: the
     page opens flush to the left edge and slides in under the menu, on every
     single navigation.

     `.page > .page-wrapper` does not mention the bar, so it holds through that
     gap and there is nothing left to animate. It is deliberately weaker (0,2,0
     against Tabler's 0,3,0): while the bar is present Tabler's selector and our
     override of it above still decide the number, and this only answers for the
     frames where neither matches. */
  .navbar-expand-lg.navbar-vertical ~ .navbar,
  .navbar-expand-lg.navbar-vertical ~ .page-wrapper,
  .page > .page-wrapper {
    margin-left: var(--qc-sidebar-w);
  }

  /* And the page moves only when somebody moves it. `data-qc-sidebar-animating`
     is set on <html> by the nav-menu controller for the length of one collapse
     or expand, and by nothing else — a transition declared unconditionally is a
     transition that any re-layout can fire, which is the paragraph above
     wearing a second hat. The bar's own width above needs no such gate: it is
     the permanent element, carried across a visit as one node, never rebuilt. */
  [data-qc-sidebar-animating] .navbar-expand-lg.navbar-vertical ~ .navbar,
  [data-qc-sidebar-animating] .navbar-expand-lg.navbar-vertical ~ .page-wrapper,
  [data-qc-sidebar-animating] .page > .page-wrapper {
    transition: margin-left var(--qc-dur-standard) var(--qc-ease-standard);
  }

  /* Peek: hovering the rail brings the full bar back *over* the page — the
     margin above stays at the rail width, so the content never re-lays itself
     out under the pointer. The lift is what says "on top of", rather than
     "pushed aside". The `nav-menu` controller owns data-qc-sidebar-peek: it
     holds the bar open while a flyout is out (the panel is `fixed`, outside the
     bar, so travelling to it reads as leaving), and sets it on focus too, so
     tabbing into a rail shows the labels.

     It sits on <html> beside data-qc-sidebar, and for the same reason that one
     does: it is *state*, and Turbo swaps the body. As a class on the bar it was
     one Turbo visit away from being lost — the bar is carried across, but
     everything the controller believed about it while it was being carried is
     not, and a click on a menu entry ended with the labels folding away under
     the pointer that had just used them. <html> is the one element no render
     touches. */
  [data-qc-sidebar="collapsed"][data-qc-sidebar-peek] .navbar-vertical.navbar-expand-lg {
    width: var(--qc-sidebar-w-full);
    box-shadow: var(--qc-shadow-lg);
  }

  /* The rail itself: a column of icons, so the icon is what the column is
     centred on. The labels are faded, not removed: the text stays in the
     accessibility tree (a rail read out as seven blank links would be useless),
     and overflow-x above does the actual hiding. This is a transition's end
     state, not `opacity` used to de-emphasise text — the thing the brand rule
     forbids.

     Fading alone left the label's *width* behind, which pinned a 24px icon into
     the left third of a 4.5rem bar. The width goes through `max-width` rather
     than `width: 0` so that it can be transitioned — `auto` is not an animatable
     value, a fixed maximum is, and it caps nothing while the bar is open: no
     label in this menu is 12rem wide. It travels over the same duration as the
     bar's own width, so the icon slides to the middle along with the edge
     instead of jumping there the moment the rail is declared. */
  /* Nothing places the icon in the rail, and that is the rule rather than an
     omission.

     It used to be centred — `justify-content: center` with the link's padding
     zeroed — and that is a switch, not a movement: it is thrown the moment the
     state is declared, against a link box that is still 15rem wide and will
     spend the next 280ms getting narrower. Every icon in the menu jumped 85px
     to the right in one frame and then crawled back left as the edge caught up
     with it. Nothing was wrong with where the icons were; the centring was
     announcing the end of a journey the bar had not started.

     Left where Tabler puts them, at a 1rem inset, they do not move at all: the
     labels are wiped by the closing edge and the icons stay under the pointer.
     That inset is within 1.5px of the middle of a 4.5rem rail, which is not a
     coincidence — the rail was measured around the icon (see the token). */

  /* Standing still also means keeping its size. At the rail the link's content
     box is 23px and the icon plus the gap after it want 28, and a flex item
     that may shrink answers that by getting smaller — the icons ended the
     collapse 15px wide instead of 20. This is the one thing in the bar that is
     the size it is. */
  .navbar-vertical .navbar-nav .nav-link-icon {
    flex: 0 0 auto;
    transition: margin-right var(--qc-dur-standard) var(--qc-ease-standard);
  }
  /* And the gap after it is room for a label there is no longer room for. It
     closes over the same 280ms rather than at once: dropped in one frame it
     pulls the label 8px left while the label is still perfectly legible. To the
     icon's right, so it cannot move the icon whatever it does. */
  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .navbar-nav .nav-link-icon {
    margin-right: 0;
  }

  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .nav-link-title {
    opacity: 0;
    max-width: 0;
    min-width: 0;
    margin: 0;
    padding: 0;
    overflow: hidden;
  }
  .navbar-vertical .nav-link-title {
    max-width: 12rem;
    white-space: nowrap;
    transition: opacity var(--qc-dur-fast) var(--qc-ease-fast),
                max-width var(--qc-dur-standard) var(--qc-ease-standard);
  }
  /* The caret would sit against the icon in a rail with nothing to point away
     from. It goes at once rather than fading, and it can: everything in the
     link is laid out from the leading edge, so what happens to the last item in
     the row cannot reach the first one. */
  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .nav-link.dropdown-toggle::after {
    display: none;
  }
}
.navbar-vertical .navbar-nav .nav-link {
  border-radius: var(--qc-radius-btn);
  margin: 2px .5rem;
  color: var(--qc-text-secondary);
  position: relative;
  transition: background-color var(--qc-dur-fast) var(--qc-ease-fast),
              color var(--qc-dur-fast) var(--qc-ease-fast);
}
.navbar-vertical .navbar-nav .nav-link:hover {
  background-color: var(--qc-layer-hover);
  color: var(--qc-text-primary);
}
.navbar-vertical .navbar-nav .nav-item.active > .nav-link,
.navbar-vertical .navbar-nav .nav-link.active {
  /* Where you are is the one item lifted off the bar: a card's surface, a
     rule round it, the short shadow. Green is for what can be pressed. */
  background: var(--qc-surface-base);
  box-shadow: 0 0 0 1px var(--qc-divider), var(--qc-shadow-sm);
  color: var(--qc-text-primary);
  font-weight: 600;
}
/* accent marker, a 5px dot — the only use of accent in the chrome */
.navbar-vertical .navbar-nav .nav-item.active > .nav-link::before,
.navbar-vertical .navbar-nav .nav-link.active::before {
  content: "";
  position: absolute;
  left: -.3rem; top: 50%; transform: translateY(-50%);
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--qc-accent);
}
.navbar-vertical .navbar-nav .nav-link-icon { color: inherit; }

/* Submenus: a flyout beside the item, opened on hover (see the `nav-menu`
   controller). Tabler lays them out as an inline accordion in the vertical bar,
   which pushed the rest of the menu down and cost a click to see what a section
   held. The panel is `fixed`, not absolute: the sidebar itself is fixed and
   scrolls (`overflow-y: scroll`), so an absolutely-placed panel would be clipped
   by that scroll — the controller places it and keeps it inside the viewport. */
@media (min-width: 992px) {
  .navbar-vertical.navbar-expand-lg .navbar-collapse .nav-item.dropdown > .dropdown-menu {
    display: none;
    position: fixed;
    z-index: 1040;
    min-width: 13rem;
    max-height: calc(100vh - 1rem);
    overflow-y: auto;
    padding: .35rem;
    background: var(--qc-surface-overlay);
    border: 1px solid var(--qc-divider);
    border-radius: var(--qc-radius-card);
    box-shadow: var(--qc-shadow-lg), var(--qc-shadow-top);
  }
  .navbar-vertical.navbar-expand-lg .navbar-collapse .nav-item.dropdown.is-open > .dropdown-menu {
    display: block;
  }
  /* The caret points where the submenu now opens. Tabler draws it as a square
     with two borders rotated -45° (down); a further quarter turn the same way
     lands it on the right. Below lg it stays pointing down, where the submenu
     still opens underneath. */
  .navbar-vertical.navbar-expand-lg .navbar-collapse .nav-item.dropdown > .nav-link.dropdown-toggle::after {
    transform: rotate(-135deg);
  }
  /* Tabler indents sub-items so they line up under the parent in the accordion;
     inside a panel of their own that indent is just dead space. */
  .navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item {
    padding: .4rem .6rem;
    border-radius: var(--qc-radius-btn);
    color: var(--qc-text-secondary);
  }
  .navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item:hover,
  .navbar-vertical.navbar-expand-lg .navbar-collapse .dropdown-menu .dropdown-item.active {
    background: var(--qc-layer-hover);
    color: var(--qc-text-primary);
  }

}
/* A rule above an entry, and nothing else.

   The settings used to be *held* at the foot of the column — `margin-top: auto`
   on the entry, which needed the three boxes above it to be columns that grow
   and the bar to be at least as tall as its own scroll box. That is gone: on a
   tall screen it left a hand's width of nothing between the settings and the
   entry above them, which is not tidiness, it is distance.

   The hairline stays, because it was never doing the same job. It says "what
   follows is a different kind of place" — what a company sets up for itself,
   rather than one more place it works — and it says it wherever the entry
   happens to fall. At every width, unlike the pin: below lg the bar is a
   collapsing panel and "at the foot" named nothing there, but a divider still
   divides.

   `hasDividerAbove()` on the entry, never a title compared against a string:
   see App\Model\Menu\Menu. */
.navbar-vertical .navbar-nav .nav-item.qc-nav-item--divided {
  margin-top: .5rem;
  padding-top: .5rem;
  /* Inset to exactly where the items are, and drawn as a background rather than
     as a border for two reasons. A `border-top` on the row would run the full
     width of the bar while every pill above and below it is held .5rem in —
     `.navbar-nav .nav-link { margin: 2px .5rem }` — so the one rule on the bar
     would be the one thing on it that is not aligned with anything. And the
     obvious way to inset it, a positioned `::before`, needs `position:
     relative` on the row: this row is a dropdown, and below lg its submenu is
     the inline list Tabler draws, which a new containing block would be free to
     move. `::before` on the link is taken by the accent marker and `::after` by
     the caret, so the rule is painted instead — one line, no layout at all. */
  background-image: linear-gradient(to right, var(--qc-divider), var(--qc-divider));
  background-repeat: no-repeat;
  background-position: .5rem top;
  background-size: calc(100% - 1rem) 1px;
}
/* Below lg the sidebar is a collapsing panel with no hover to speak of, so the
   submenu stays the inline list Tabler draws — opened by tapping its item. */
@media (max-width: 991.98px) {
  .navbar-vertical .navbar-collapse .nav-item.dropdown > .dropdown-menu { display: none; }
  .navbar-vertical .navbar-collapse .nav-item.dropdown.is-open > .dropdown-menu { display: block; }

  /* Phone header: burger, then the mark right beside it. The bar is a flex row
     with `justify-content: space-between`, which pinned the logo to the far
     right once the user block that used to fill the middle was removed —
     `margin-right: auto` gives that empty space back to the right side. The
     `auto` side margins that centre the mark in the desktop column would
     re-centre it here, so they are reset. */
  .navbar-vertical .navbar-brand {
    margin-left: .5rem;
    margin-right: auto;
  }
  .navbar-vertical .navbar-brand-image { margin: 0; }
}

/* Brand logo — Tabler caps .navbar-brand-image at 2rem, which shrank the mark.
   Give it real presence in both the vertical sidebar and the legacy header.

   Both dimensions stay `auto` and are bounded by maxima instead: the wordmark
   is 4.26:1, wider at 48px tall than the room left beside the collapse button
   in a 15rem column, and a fixed `height` with `max-width: 100%` over it is
   exactly the pair that squashes an image — the width gets clamped and the
   height does not follow. With both auto the browser keeps the ratio and picks
   whichever bound it meets first; `object-fit` is the belt to that braces, in
   case a page ever hands the element a box of its own. */
.navbar-brand-image {
  height: auto;
  width: auto;
  max-height: 44px;
  max-width: 100%;
  object-fit: contain;
}
.navbar-vertical .navbar-brand { padding: .5rem 0; }
.navbar-vertical .navbar-brand-image { max-height: 48px; margin: 0 auto; }

/* ---- Sidebar head: the mark, and the control that collapses the bar ----
   `display: contents` by default, so below lg the brand and the button are once
   again direct children of the bar's flex row and every phone-header rule above
   (burger first, mark right beside it) keeps applying to the same elements it
   was written for. The wrapper only becomes a box where it has a job to do. */
.qc-sidebar-head { display: contents; }

@media (min-width: 992px) {
  /* A box of a stated height, and nothing inside it is allowed to decide that
     height. This is the whole fix for the menu jumping when the rail peeks:
     the head used to be as tall as whatever it happened to contain, and what it
     contained was an image bounded by `max-width: 100%` of a column that is
     travelling from 4.5rem to 15rem. The moment the peek was declared, the
     wordmark replaced the symbol inside a box still only 72px wide and rendered
     5px tall — the head collapsed from 80px to 52px in one frame, taking every
     menu item 28px up with it, and then let them back down over the 280ms the
     width took to arrive. Both logos are laid *inside* this box now, and the
     box does not move. */
  .qc-sidebar-head {
    position: relative;
    display: flex;
    align-items: center;
    height: var(--qc-sidebar-head-h);
    padding: 0 var(--qc-sidebar-head-pad);
    /* Square, so this is its width too: the mark is `logo_symbol.png`. */
    --qc-brand-mark-size: 3rem;
    /* The lockup is 4.264:1, so 2.75rem tall is 187px wide — it clears the
       collapse button in a 15rem column with room to spare. */
    --qc-brand-full-h: 2.75rem;
  }
  /* The brand fills the head, in every state, so that the whole strip left of
     the button is the link to the dashboard — and so that neither logo is
     measured against a box whose width depends on whether the button is out. */
  .qc-sidebar-head .navbar-brand {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    height: 100%;
    margin: 0;
    padding: 0;
  }
  .qc-sidebar-head .navbar-brand > a {
    position: absolute;
    inset: 0;
  }

  /* Both marks are in the same place at the same time, one of them transparent.
     Taken out of flow they cannot lay the head out, and given a height of their
     own they no longer shrink with the column: while the bar widens the wordmark
     is simply uncovered by the moving edge (the bar clips its own overflow-x),
     which is the reveal we wanted rather than a logo inflating from 5px. */
  .qc-sidebar-head .navbar-brand-image {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: auto;
    max-width: none;
    max-height: none;
    margin: 0;
    transition: opacity var(--qc-dur-fast) var(--qc-ease-fast);
  }
  .qc-sidebar-head .qc-brand-full {
    left: 0;
    height: var(--qc-brand-full-h);
  }
  /* Centred on the rail — measured off the rail's own width rather than off the
     brand's, which is the width that moves. Fading out over a box that is
     doubling would have slid the symbol across the bar on its way out. */
  .qc-sidebar-head .qc-brand-mark {
    display: block;
    height: var(--qc-brand-mark-size);
    left: calc((var(--qc-sidebar-w-rail)
                - 2 * var(--qc-sidebar-head-pad)
                - var(--qc-brand-mark-size)) / 2);
  }

  /* The swap. `display` cannot be crossed from one to the other — it is a step,
     and a step is what put the wrong logo in a 72px box for the length of a
     transition. Opacity is the same decision made continuously.

     The one arriving waits for the one leaving. Both files carry the symbol and
     they do not carry it at the same size — the rail's is the whole picture,
     the lockup's is a sixth of it — so at the middle of an even cross-fade you
     see one inside the other and it reads as a smudge. Leaving first, arriving
     second, and the delay plus the fade come to 280ms: the wordmark finishes
     exactly as the edge stops. The delay is read off the state being entered,
     so putting it on whichever of the two is *appearing* in that state gets the
     order right in both directions. */
  .qc-sidebar-head .qc-brand-mark { opacity: 0; }
  .qc-sidebar-head .qc-brand-full { transition-delay: var(--qc-dur-press); }
  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .qc-brand-full {
    opacity: 0;
    transition-delay: 0s;
  }
  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .qc-brand-mark {
    opacity: 1;
    transition-delay: var(--qc-dur-press);
  }

  /* The button is placed where it belongs in the *open* bar and left there, out
     of the flow. Two things went wrong when it was a flex item at the end of
     the row: it took 28px away from the brand at the instant the peek was
     declared — a frame before the bar itself moved — and then it rode the right
     edge for the whole 280ms, which is a chevron flying left to right straight
     across the wordmark. Anchored from the left it is only ever uncovered by
     the edge on its way past, like the wordmark beside it.

     The rail still carries no control of its own — it fades rather than
     disappears, because a button switched on by `display` arrives at full
     strength on a bar that is not open yet. `visibility` rides along so the
     rail's chevron is neither focusable nor announced; it is the one property
     here whose step *is* the behaviour we want, and CSS holds it to the end of
     the fade in the direction where that matters. */
  .qc-sidebar-head .qc-sidebar-toggle {
    position: absolute;
    top: 50%;
    left: calc(var(--qc-sidebar-w-full)
               - var(--qc-sidebar-head-pad)
               - var(--qc-sidebar-toggle-size));
    transform: translateY(-50%);
  }
  [data-qc-sidebar="collapsed"]:not([data-qc-sidebar-peek]) .navbar-vertical .qc-sidebar-toggle {
    opacity: 0;
    visibility: hidden;
  }
}
.qc-brand-mark { display: none; }

/* A quiet chevron beside the mark, not a 44px glass disc: this is chrome for
   the chrome, and at the head of the menu it was reading as the first item of
   it — and it was taking the width the wordmark needed. 28px is still over the
   24px floor for a pointer target, and it is a desktop-only control.

   Desktop only — below lg the burger already opens and closes this bar, and two
   controls for one panel is one too many. Done here rather than with `d-none
   d-lg-inline-flex` on the button, because those utilities are `!important` and
   the rail rule above could then never take the button away. */
.qc-sidebar-toggle {
  /* Named because the head places the button by arithmetic, not by its right
     edge — see §5.4 — and that sum has to know how wide it is. */
  --qc-sidebar-toggle-size: 1.75rem;   /* 28px */
  display: none;
  flex: 0 0 auto;
  width: var(--qc-sidebar-toggle-size);
  height: var(--qc-sidebar-toggle-size);
  box-shadow: none;
  transition: opacity var(--qc-dur-fast) var(--qc-ease-fast),
              visibility var(--qc-dur-fast);
}
@media (min-width: 992px) {
  .qc-sidebar-head .qc-sidebar-toggle { display: inline-flex; }
}
/* The arrow follows the *stored* state, not the peek: while the bar is only
   peeking it still reads "expand", which is the truth — the click is what makes
   it stay. */
.qc-sidebar-toggle .ti {
  font-size: 16px;
  line-height: 1;
  transition: transform var(--qc-dur-standard) var(--qc-ease-standard);
}
[data-qc-sidebar="collapsed"] .qc-sidebar-toggle .ti { transform: rotate(180deg); }

/* The bar across the top of every page — the trail, the search and the actions —
   is public/css/quercia/topbar.css. `.page-header` survives only inside the two
   submission cards (work/new_submission, work/submission), which re-use Tabler's
   header markup within a card and are not this shell's header. */

@media (max-width: 575.98px) {
  /* Card footers: cancel on one side, the save buttons pushed to the other, in a
     row that is not allowed to wrap. On a phone neither side can drop to a line
     of its own, so the row squeezes them instead — and a button squeezed past
     its text sets that text one letter per line, which is what the save buttons
     on the work form were doing. Letting the row wrap gives each side a line. */
  .card-footer > .d-flex {
    flex-wrap: wrap;
    gap: .5rem;
  }
  .card-footer > .d-flex > .ms-auto {
    width: 100%;
    margin-left: 0 !important;
  }
}

/* The previewed sheet keeps what is on it, on it.
   The "anteprima" watermark is absolutely positioned, set at display size and
   turned 70 degrees, which leaves its box sticking out of the card on every
   side. An absolute box does not disturb the layout but it does still overflow,
   and an overflow wide enough gives the page a sideways scroll — which on a
   phone carried the modal off the screen. The sheet is the edge of the
   document, so it is where the clipping belongs. */
.qc-doc-sheet { overflow: hidden; }

/* The document cannot push the page around, whatever is written in it.
   Item descriptions and the closing note are raw HTML from an editor, so the
   preview renders text nobody vetted for width: an address, a long word, a
   pasted image. Any of them makes the table's smallest possible width larger
   than the screen, and since a modal is laid out against the page, a page given
   a sideways scroll takes the modal off the edge with it.
   The scroll is the backstop for whatever still will not fit: it keeps the
   overflow inside the document, where it costs a swipe, rather than letting it
   move the window. */
.qc-doc-preview {
  overflow-x: auto;
}
.qc-doc-preview img {
  max-width: 100%;
  height: auto;
}
/* Only the written parts may be broken mid-word, and only they need to be:
   they are the ones whose width nobody chose. `anywhere` rather than
   `break-word` because only `anywhere` counts towards how narrow the browser
   will allow the column to become, and that measurement is what overflowed.
   Set on the whole document it also applied to the amounts, which then broke a
   digit per line and read as a column of numerals — the cure being worse than
   the ailment it was mixed for. */
.qc-doc-preview tbody td:nth-child(2),
.qc-doc-preview address,
.qc-doc-preview p {
  overflow-wrap: anywhere;
}
/* An amount is one thing and is read in one piece. Nothing here is long enough
   for that to cost width worth having. */
.qc-doc-preview .qc-estdoc__num {
  white-space: nowrap;
}

/* Document preview (estimate) on a phone.
   The preview stands for an A4 sheet, and on a 360px screen an A4 layout does
   not fit: the five-column table ran off the side and was cut. Rather than show
   a page too small to read or one that has to be panned, the preview reflows —
   the head stacks, and the table drops the two columns that carry the least.
   The line number is scaffolding for reading across a wide row, which is the
   one thing a narrow row does not need; the unit price is recoverable from the
   quantity and the line total beside it. What stays is what a preview is
   checked for: what was quoted, how much of it, and for how much.
   This is the preview only. The PDF keeps its columns — the two are meant to
   differ here, readability being worth more on a phone than a faithful
   miniature of a page nobody could read at that size. */
@media (max-width: 575.98px) {
  .qc-doc-preview thead th:nth-child(1),
  .qc-doc-preview tbody tr.qc-estdoc__line td:nth-child(1),
  .qc-doc-preview thead th:nth-child(4),
  .qc-doc-preview tbody tr.qc-estdoc__line td:nth-child(4) {
    display: none;
  }
  /* The title is set for a sheet; at this width it took four lines of its own. */
  .qc-doc-preview h1 { font-size: var(--qc-fs-2xl); }
}

/* ================================================================== *
 * Modals — one coherent brand chrome for EVERY modal in the app
 * (.modal-content / .dropdown-menu surfaces are styled in §5.3 above)
 * ================================================================== */
.modal-header {
  border-bottom: 1px solid var(--qc-divider);
  padding: 1rem 1.25rem;
}
.modal-title {
  font-family: var(--qc-font-display);
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--qc-text-primary);
}
.modal-body { padding: 1.25rem; }
.modal-footer {
  border-top: 1px solid var(--qc-divider);
  padding: .9rem 1.25rem;
  gap: .5rem;
}
.modal-footer > * { margin: 0; }
.btn-close {
  border-radius: var(--qc-radius-input);
  opacity: .7;
  transition: opacity var(--qc-dur-fast) var(--qc-ease-fast),
              background-color var(--qc-dur-fast) var(--qc-ease-fast);
}
.btn-close:hover {
  opacity: 1;
  background-color: var(--qc-layer-selected);
}

/* ------------------------------------------------------------------ *
 * QcDialog — the single confirmation / prompt / notice dialog
 *
 * Two custom properties carry the whole severity story, so the variants below
 * are one line each. They are kept apart on purpose: `tint` is the hue as a
 * FILL (the status bar, the icon disc), `ink` is the same meaning as a MARK on
 * that pale disc. Raw --qc-warning on the dialog surface sits near 1.6:1, well
 * under the 3:1 a meaningful graphic owes the reader, so the glyph takes the
 * text-safe variant instead. Never Tabler's .bg-info and friends here: the
 * token layer remaps --tblr-azure and --tblr-blue to forest green, and an
 * informational dialog would come out wearing the brand colour.
 * ------------------------------------------------------------------ */
.qc-dialog {
  --qc-dialog-tint: var(--qc-error);
  --qc-dialog-ink: var(--qc-error-text);
}

/* The question is always the top layer.
 *
 * Bootstrap gives every .modal the same z-index (1055) and every backdrop 1050,
 * so two open modals are separated by document order alone — and this host is
 * included before the job sheet's, which is the one thing in the application
 * that asks a question from inside a modal. A .modal is a full-viewport fixed
 * box, so the sheet did not merely paint over the dialog: it swallowed every
 * click meant for it, and a click landing on the sheet's own container reads as
 * a click outside its panel and closes it. The question was answerable only
 * with the keyboard, and "Approva" from the job sheet did nothing at all.
 *
 * Above the modal layer, then, with its own veil above the modal it was asked
 * over — a dialog that dims nothing over a panel that is still lit does not
 * read as blocking. The class on the backdrop is put there by qc_dialog.js,
 * which knows which one it just caused; the number stays here with the rest of
 * the layering. Below Bootstrap's tooltip (1080) and toast (1090): a toast
 * reporting what happened must not end up under the dialog that asked. */
.qc-dialog.modal { z-index: 1070; }
.modal-backdrop.qc-dialog-backdrop { z-index: 1065; }
.qc-dialog[data-qc-variant="warning"] { --qc-dialog-tint: var(--qc-warning); --qc-dialog-ink: var(--qc-warning-text); }
.qc-dialog[data-qc-variant="info"]    { --qc-dialog-tint: var(--qc-info);    --qc-dialog-ink: var(--qc-info-text); }
.qc-dialog[data-qc-variant="success"] { --qc-dialog-tint: var(--qc-success); --qc-dialog-ink: var(--qc-success-text); }
.qc-dialog[data-qc-variant="primary"] { --qc-dialog-tint: var(--qc-primary); --qc-dialog-ink: var(--qc-primary); }

/* The severity band across the top. Tabler's own .modal-status, restated on
   brand tokens. It has no radius of its own: the dialog clips it to its curve.
   Rounded to the card radius, it stuck out past the corners of a modal whose
   radius is larger — the band has to follow whatever curve the dialog has,
   not guess it. */
.qc-dialog .modal-content { overflow: hidden; }
.qc-dialog .qc-dialog__status {
  height: 3px;
  background: var(--qc-dialog-tint);
}

.qc-dialog .qc-dialog__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  margin-bottom: .75rem;
  border-radius: var(--qc-radius-pill);
  color: var(--qc-dialog-ink);
  background: color-mix(in srgb, var(--qc-dialog-tint) 16%, transparent);
}
.qc-dialog .qc-dialog__icon svg { width: 1.5rem; height: 1.5rem; }
.qc-dialog .qc-dialog__preview { margin-top: 1rem; }
/* Three answers (QcDialog.choose) stack, the one that keeps the work on top:
   keep, lose, stay. Two stay side by side, cancel first. */
.qc-dialog .qc-dialog__acts.is-stacked > .col { flex: 0 0 100%; }
.qc-dialog .qc-dialog__acts.is-stacked > .qc-dialog__act--confirm { order: 1; }
.qc-dialog .qc-dialog__acts.is-stacked > .qc-dialog__act--alt { order: 2; }
.qc-dialog .qc-dialog__acts.is-stacked > .qc-dialog__act--cancel { order: 3; }

.qc-dialog .qc-dialog__message {
  font-family: var(--qc-font-display);
  font-weight: 700;
  font-size: var(--qc-fs-xl);
  letter-spacing: -.01em;
  color: var(--qc-text-primary);
  margin-bottom: .5rem;
  /* A confirmation is usually one sentence written for a person, not a label:
     let it wrap and keep the words whole. */
  text-wrap: balance;
}
.qc-dialog .qc-dialog__detail {
  margin-bottom: 0;
  font-size: var(--qc-fs-md);
  line-height: 1.4;
  /* Callers join supporting lines with "\n"; honouring it keeps the message a
     plain string, so nothing has to hand markup to the dialog. */
  white-space: pre-line;
}
.qc-dialog .qc-dialog__field { margin-top: 1rem; text-align: start; }
.qc-dialog .modal-footer { border-top: 0; padding-top: 0; }

/* The entrance is deliberately not restated here. Tabler already gives
   .modal-dialog `transform .3s ease-out` and already switches it off under
   prefers-reduced-motion, so an override only risks confusing Bootstrap, which
   measures that same transition to decide when the modal has finished opening.

   The blur behind it is .modal-blur — the dialog is one of the few places the
   brand spends a real backdrop-filter (rule 3): it is an overlay, and there is
   only ever one on screen. */

/* ================================================================== *
 * Alerts — soft brand tint + left accent, per semantic (whole app)
 * ================================================================== */
.alert {
  border-radius: var(--qc-radius-card);
  border: 1px solid var(--qc-border);
  border-left-width: 3px;
  color: var(--qc-text-primary);
  background: var(--qc-surface-light);
}
/* Alert heading. This was `.alert-title`, which Tabler 1.x dropped for
   `.alert-heading` — the one class, of the 788 the app uses, that the jump from
   beta19 to 1.4.0 took away. The replacement carries only margin and weight
   where the old one also set size and line height, so both are restated here;
   without them the 17 alert headings around the app would deflate to body text. */
.alert .alert-heading {
  font-size: var(--qc-fs-md);
  line-height: 1.25rem;
  font-weight: 600;
  margin-bottom: .25rem;
  color: var(--qc-text-primary);
}
.alert a:not(.btn), .alert .alert-link { color: var(--qc-primary); font-weight: 600; }
.alert-success, .alert-green {
  background: color-mix(in srgb, var(--qc-success) 14%, var(--qc-surface));
  border-color: color-mix(in srgb, var(--qc-success) 38%, transparent);
  border-left-color: var(--qc-success);
}
.alert-danger, .alert-red, .alert-important {
  background: color-mix(in srgb, var(--qc-error) 13%, var(--qc-surface));
  border-color: color-mix(in srgb, var(--qc-error) 40%, transparent);
  border-left-color: var(--qc-error);
}
.alert-warning, .alert-yellow, .alert-orange {
  background: color-mix(in srgb, var(--qc-warning) 16%, var(--qc-surface));
  border-color: color-mix(in srgb, var(--qc-warning) 45%, transparent);
  border-left-color: var(--qc-warning);
}
.alert-info, .alert-blue, .alert-azure, .alert-cyan {
  background: color-mix(in srgb, var(--qc-info) 14%, var(--qc-surface));
  border-color: color-mix(in srgb, var(--qc-info) 40%, transparent);
  border-left-color: var(--qc-info);
}

/* ================================================================== *
 * The person, at the end of the page-header row
 * (markup: templates/base/_user_menu.html.twig)
 * ================================================================== */

/* The toggle borrows the sidebar's own quantities — primary 8% behind it,
   text-primary on top — so the two pieces of chrome answer a pointer the same
   way. Transparent at rest: the corner belongs to the page until you reach for
   it. prefers-reduced-motion is neutralised globally in tokens.css. */
.qc-usermenu__toggle {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  /* .5rem of vertical padding around a 2rem avatar clears the 44px target the
     brand asks of anything a thumb has to find. */
  padding: .375rem .625rem .375rem .375rem;
  border: 1px solid transparent;
  border-radius: var(--qc-radius-pill);
  background-color: transparent;
  color: var(--qc-text-secondary);
  font-weight: 600;
  line-height: 1;
  transition: background-color var(--qc-dur-fast) var(--qc-ease-fast),
              color var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-usermenu__toggle:hover,
.qc-usermenu__toggle[aria-expanded="true"] {
  background-color: var(--qc-layer-hover);
  color: var(--qc-text-primary);
}
.qc-usermenu__toggle:focus-visible {
  outline: 2px solid var(--qc-primary);
  outline-offset: 2px;
}
.qc-usermenu__name {
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.qc-usermenu__caret {
  font-size: 1rem;
  color: var(--qc-text-tertiary);
}

.qc-usermenu__panel { min-width: 15rem; }
.qc-usermenu__head {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  padding: .75rem 1rem .5rem;
}
.qc-usermenu__person {
  font-weight: 600;
  color: var(--qc-text-primary);
}
.qc-usermenu__panel .dropdown-item {
  display: flex;
  align-items: center;
  gap: .625rem;
}
.qc-usermenu__panel .dropdown-item > .ti {
  font-size: 1.125rem;
  color: var(--qc-text-tertiary);
}
.qc-usermenu__panel .dropdown-item:hover > .ti { color: inherit; }

/* The rule that separated the person from the page's own actions moved to
   topbar.css with them: the actions now have a line of their own, so on most
   pages there is nothing to the person's left to be separated from. */

/* ================================================================== *
 * Initials avatar — replaces the unused leaf photo (brand gradient)
 * ================================================================== */
.avatar.qc-avatar {
  /* From primary, not primary-light, and in on-primary: white on the lighter
     green was 4.4:1 in light, and 1.4:1 once the dark theme made it mint. */
  background: linear-gradient(135deg, var(--qc-primary), var(--qc-primary-dark));
  color: var(--qc-on-primary);
  font-family: var(--qc-font-display);
  font-weight: 700;
  letter-spacing: .01em;
}

/* ================================================================== *
 * Flash toasts — branded (surface + status accent + icon), see
 * templates/base/flash.html.twig. Replaces Bootstrap text-bg-* solids.
 * ================================================================== */
.qc-toast {
  display: flex;
  align-items: flex-start;
  gap: .7rem;
  width: 340px;
  max-width: calc(100vw - 2rem);
  padding: .85rem .95rem;
  margin-bottom: .6rem;
  background: var(--qc-surface-overlay);
  border: 1px solid var(--qc-edge);
  border-radius: var(--qc-radius-card);
  box-shadow: var(--qc-shadow-lg), var(--qc-shadow-top);
  color: var(--qc-text-primary);
  position: relative;
  overflow: hidden;
}
.qc-toast::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; background: var(--tc, var(--qc-primary));
}
.qc-toast__icon { flex: none; color: var(--tc, var(--qc-primary)); margin-top: 1px; }
.qc-toast__icon svg { width: 20px; height: 20px; display: block; }
/* A column, not a run of text: the action was an inline box after an inline
   sentence and ended up wherever the last word did. */
.qc-toast__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  font-size: var(--qc-fs-md);
  line-height: 1.4;
  text-wrap: balance;
}
.qc-toast__body strong { display: block; margin-bottom: .1rem; font-weight: 600; }
.qc-toast .btn-close { flex: none; margin-left: .25rem; }
/* The one button a toast may carry: undoing the gesture it reports
   (showToast's `options.action`, the calendar's move). Under the sentence,
   left-aligned, so it reads as the answer to it. */
.qc-toast__action { margin-top: .5rem; }
.qc-toast--success { --tc: var(--qc-success); }
.qc-toast--danger  { --tc: var(--qc-error); }
.qc-toast--warning { --tc: var(--qc-warning); }
.qc-toast--info    { --tc: var(--qc-info); }

/* =========================================================================
   Theme picker — account settings selector with micro previews
   (auto / light / dark). Mini windows render each palette via a nested
   [data-bs-theme] context so they always show the real theme colours,
   independent of the page's current theme.
   ========================================================================= */
/* Sober, and small on purpose. A preview is a thumbnail of an answer, not the
   answer: at 76px tall with a 2px border and a 14px radius these were the
   largest thing on the account page, and they were showing a choice most people
   make once. The card is now the size of a chip that happens to have a picture
   in it, and the picker is capped so two of them do not spread across the
   measure. */
.qc-theme-picker {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: .5rem;
    max-width: 26rem;
}
.qc-theme-picker--pair { grid-template-columns: repeat(2, 1fr); max-width: 18rem; }
@media (max-width: 540px) { .qc-theme-picker { grid-template-columns: 1fr; } }
.qc-theme-picker__option { margin: 0; }
.qc-theme-picker input[type="radio"] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.qc-theme-picker__card {
    display: block; cursor: pointer; margin: 0;
    border: 1px solid var(--qc-border); border-radius: var(--qc-radius-btn); padding: .375rem;
    background: var(--qc-card-bg);
    transition: border-color var(--qc-dur-fast) var(--qc-ease-fast),
                box-shadow var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-theme-picker__card:hover { border-color: var(--qc-primary-light); }
.qc-theme-picker input:checked + .qc-theme-picker__card {
    border-color: var(--qc-primary);
    box-shadow: 0 0 0 2px rgba(var(--qc-primary-rgb), .18);
}
.qc-theme-picker input:focus-visible + .qc-theme-picker__card {
    outline: 2px solid var(--qc-primary); outline-offset: 2px;
}
.qc-theme-picker__label {
    display: flex; align-items: center; justify-content: center; gap: .3rem;
    margin-top: .375rem; font-weight: 600; font-size: var(--qc-fs-xs); color: var(--qc-text-secondary);
}
.qc-theme-picker input:checked + .qc-theme-picker__card .qc-theme-picker__label { color: var(--qc-text-primary); }
.qc-theme-picker__label .ti { font-size: .875rem; line-height: 1; }
@media (prefers-reduced-motion: reduce) {
    .qc-theme-picker__card { transition: none; }
}

/* Mini window mock */
.qc-theme-mini {
    position: relative; height: 50px; border-radius: 6px; overflow: hidden;
    border: 1px solid var(--qc-border); background: var(--qc-background); display: flex;
}
.qc-theme-mini__side {
    width: 28%; background: var(--qc-surface-sunken); border-right: 1px solid var(--qc-border);
    padding: 5px 3px; display: flex; flex-direction: column; gap: 3px;
}
.qc-theme-mini__body { flex: 1; padding: 5px 4px; display: flex; flex-direction: column; gap: 3px; }
.qc-theme-mini__bar { height: 3px; border-radius: 2px; background: var(--qc-border); }
.qc-theme-mini__bar--brand { background: var(--qc-primary); width: 72%; }
.qc-theme-mini__card { height: 14px; border-radius: 3px; background: var(--qc-card-bg); border: 1px solid var(--qc-border); }
.qc-theme-mini__line { height: 3px; border-radius: 2px; background: var(--qc-border); }
.qc-theme-mini__line--short { width: 55%; }

/* auto = diagonal light/dark split (two stacked layers) */
.qc-theme-mini__layer { position: absolute; inset: 0; display: flex; }
.qc-theme-mini__layer--dark { clip-path: polygon(58% 0, 100% 0, 100% 100%, 42% 100%); }

/* Mini table mock, for the density picker. Each card shows its OWN density
   rather than the one currently in force, so the two sit side by side as a
   comparison — the same reasoning as the theme minis rendering their own
   palette. The proportions are the --qc-grid-* ones scaled down. */
.qc-density-mini {
    --mini-row-h: 8px;
    --mini-gap: 3px;
    height: 50px;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid var(--qc-border);
    background: var(--qc-surface);
    padding: 4px 4px;
    display: flex;
    flex-direction: column;
    gap: var(--mini-gap);
}
.qc-density-mini--compact {
    --mini-row-h: 5px;
    --mini-gap: 1px;
}
.qc-density-mini__head,
.qc-density-mini__row {
    display: flex;
    align-items: center;
    gap: 4px;
    height: var(--mini-row-h);
    flex: none;
}
.qc-density-mini__head { border-bottom: 1px solid var(--qc-border); padding-bottom: 2px; }
.qc-density-mini__cell {
    height: 3px;
    border-radius: 2px;
    background: var(--qc-border);
    flex: 1;
}
.qc-density-mini__cell--wide { flex: 2.2; }
/* The numeric column reads as right-aligned even here: it is the point of the
   table, not decoration. */
.qc-density-mini__cell--num { flex: .8; margin-left: auto; max-width: 22%; }
.qc-density-mini__head .qc-density-mini__cell { background: var(--qc-text-tertiary); height: 2px; }
.qc-density-mini__row .qc-density-mini__cell--num { background: var(--qc-primary); }

/* =========================================================================
   The job's frame — one header for its three pages (templates/work/_frame).
   The switcher first — Lavoro · Preventivo · Rapportino, one shape on all
   three pages — then the status (a menu of where the job can go, for whoever
   plans), the job's name, where and when and who, and on the right one slot
   for the part's own buttons.
   ========================================================================= */
/* The three parts of a job share one column, so the switcher keeps one shape
   from page to page: the job page, the estimate and the report all sit in it.
   On the left like every other form page (.qc-formpage), never centred. */
.qc-jobpage { width: 100%; max-width: 68rem; }
/* No rule of its own under it: what follows — the steps, the report's blocks —
   draws its own edge. */
.qc-frame { padding-bottom: 1.25rem; }
.qc-frame__head { display: flex; align-items: flex-start; gap: .75rem 1rem; flex-wrap: wrap; }
.qc-frame__status { flex: none; margin-top: .125rem; }
/* The pill is the control: no button chrome of its own, only the chevron and
   the focus ring a control owes. */
.qc-frame__statusbtn {
    display: inline-flex; align-items: center; gap: .25rem;
    padding: 0; border: 0; background: transparent; color: var(--qc-text-secondary);
    border-radius: var(--qc-radius-pill); cursor: pointer;
}
.qc-frame__statusbtn:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }
.qc-frame__statusbtn:hover .qc-stpill { box-shadow: 0 0 0 2px var(--qc-layer-highlight); }
.qc-frame__ident { flex: 1 1 18rem; min-width: 0; }
.qc-frame__title {
    margin: 0;
    font-family: var(--qc-font-display); font-weight: 700; letter-spacing: -.015em;
    font-size: var(--qc-fs-2xl); line-height: 1.25; color: var(--qc-text-primary);
}
.qc-frame__num { color: var(--qc-text-secondary); }
.qc-frame__facts {
    display: flex; flex-wrap: wrap; gap: .25rem 1rem;
    margin: .375rem 0 0; padding: 0; list-style: none;
    font-size: var(--qc-fs-sm); color: var(--qc-text-secondary);
}
.qc-frame__facts li { display: inline-flex; align-items: center; gap: .375rem; }
.qc-frame__facts .ti { color: var(--qc-text-tertiary); }
.qc-frame__chips { display: flex; flex-wrap: wrap; gap: .375rem; margin-top: .5rem; }
.qc-frame__chips > span:empty { display: none; }
.qc-frame__acts { flex: none; margin-left: auto; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.qc-frame__more { flex: none; }
.qc-frame__moves, .qc-frame__more .dropdown-menu { min-width: 14rem; }
/* A form's submit, drawn as a menu row: the theme draws every submit with
   `.btn`, and inside a menu it is a row like any other item. */
.qc-frame__move,
.qc-frame__move.btn {
    display: flex; align-items: center; justify-content: flex-start; gap: .6rem;
    width: 100%; padding: .5rem .75rem; border: 0; border-radius: var(--qc-radius-input);
    background: transparent; box-shadow: none; backdrop-filter: none;
    color: var(--qc-text-primary); font-weight: 500; text-align: start;
}
.qc-frame__move:hover,
.qc-frame__move:focus-visible {
    background: var(--qc-layer-selected);
    color: var(--qc-text-primary);
}
.qc-frame__delete, .qc-frame__delete:hover { color: var(--qc-error-text); }
.qc-frame__dot {
    flex: none; width: .625rem; height: .625rem; border-radius: 50%;
    background: var(--qc-dot, var(--qc-primary));
}

/* The switcher: the three parts of a job, each a page of its own. A well
   with the open part raised out of it, ringed underneath in primary. */
.qc-parts {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px;
    margin-bottom: 1.25rem; padding: 4px;
    border-radius: var(--qc-radius-card);
    background: var(--qc-surface-well);
    box-shadow: inset 0 0 0 1px var(--qc-divider);
}
.qc-part {
    position: relative;
    display: flex; align-items: center; gap: .75rem; min-width: 0; min-height: 3.75rem;
    padding: .5rem .875rem;
    border: 1px solid transparent; border-radius: var(--qc-radius-btn);
    color: var(--qc-text-secondary); text-decoration: none;
    transition: background-color var(--qc-dur-fast) var(--qc-ease-fast);
}
a.qc-part:hover { background: var(--qc-layer-hover); color: var(--qc-text-primary); }
a.qc-part:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }
.qc-part__icon {
    flex: none; display: grid; place-items: center;
    width: 2.25rem; height: 2.25rem; border-radius: var(--qc-radius-input);
    background: var(--qc-surface-base); border: 1px solid var(--qc-divider);
    color: var(--qc-text-secondary); font-size: 1.125rem;
}
.qc-part__text { display: flex; flex-direction: column; min-width: 0; }
.qc-part__name { font-size: var(--qc-fs-lg); font-weight: 700; color: var(--qc-text-primary); }
.qc-part__name .ti { font-size: .875rem; color: var(--qc-text-tertiary); }
.qc-part__line {
    font-size: var(--qc-fs-sm); color: var(--qc-text-secondary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* What the job still lacks, in the warning's ink: words, never colour alone. */
.qc-part__line--warn { color: var(--qc-warning-text); font-weight: 600; }
.qc-part.is-on {
    background: var(--qc-surface-base);
    border-color: var(--qc-border);
    box-shadow: var(--qc-shadow-md), var(--qc-shadow-top);
}
.qc-part.is-on .qc-part__icon { background: var(--qc-layer-wash); border-color: transparent; color: var(--qc-primary-ink); }
.qc-part.is-on .qc-part__name { color: var(--qc-primary-ink); }
.qc-part.is-on::after {
    content: ""; position: absolute; left: .875rem; right: .875rem; bottom: -1px;
    height: 3px; border-radius: 3px 3px 0 0; background: var(--qc-primary);
}
/* A part the job does not have: said, not pressable. */
.qc-part.is-off .qc-part__name { color: var(--qc-text-secondary); }
@media (max-width: 767px) {
    /* Three narrow parts: icon and name, the line goes. */
    .qc-part { flex-direction: column; justify-content: center; gap: .25rem; padding: .5rem .375rem; text-align: center; }
    .qc-part__line { display: none; }
    .qc-part__name { font-size: var(--qc-fs-sm); }
    .qc-frame__title { font-size: var(--qc-fs-xl); }
    /* The status and the part's buttons share the first line; the name has the next. */
    .qc-frame__acts { order: 2; }
    .qc-frame__ident { order: 3; flex-basis: 100%; }
}
@media (prefers-reduced-motion: reduce) {
    .qc-part { transition: none; }
}

/* The save line beside a page's buttons: saved, or changes waiting. Which one
   shows is the form's own `data-dirty` (work_schedule_controller). */
.qc-savestate {
    display: inline-flex; align-items: center; gap: .375rem;
    font-size: var(--qc-fs-sm); color: var(--qc-text-secondary);
}
.qc-savestate .ti { color: var(--qc-primary); }
.qc-savestate--dirty { display: none; color: var(--qc-warning-text); font-weight: 600; }
.qc-savestate--dirty::before {
    content: ""; width: .5rem; height: .5rem; border-radius: 50%;
    background: var(--qc-warning);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--qc-warning) 30%, transparent);
}
form[data-dirty="true"] .qc-savestate--clean { display: none; }
form[data-dirty="true"] .qc-savestate--dirty { display: inline-flex; }

/* ============================================================================
   Category visual language — operatori (green) / servizi (bronze) / mezzi (blue).
   Single-sourced here; used by the dashboard, the calendar modal and the work
   tables (renderers crewChips / typePills). Colours resolve from tokens so they
   auto-adapt to light/dark.
   ============================================================================ */
.catwrap { display: inline-flex; flex-wrap: wrap; gap: .3rem; align-items: center; max-width: 100%; }

/* Operatori: pill chip with a green initials avatar */
.qc-cm { display: inline-flex; align-items: center; gap: .4rem; padding: .2rem .6rem .2rem .2rem; border-radius: var(--qc-radius-pill); background: var(--qc-surface-light); border: 1px solid var(--qc-border); font: 600 var(--qc-fs-sm) var(--qc-font-body); color: var(--qc-text-primary); white-space: nowrap; max-width: 100%; }
.qc-cm__name { overflow: hidden; text-overflow: ellipsis; }
.qc-ini { min-width: 22px; width: auto; height: 22px; padding: 0 4px; border-radius: var(--qc-radius-pill); display: grid; place-items: center; font: 800 var(--qc-fs-xs) var(--qc-font-body); color: var(--qc-on-primary); background: linear-gradient(135deg, var(--qc-primary-light), var(--qc-primary-dark)); flex: none; }
/* Standalone avatar (work tables show only the abbreviation; full name on hover) */
.qc-ini--solo { cursor: default; }
/* A row of people in a grid cell stays one line: every row is one height, so
   a second line of initials would be cut in half. The column is sized for
   the crew instead (the customer sheet's `minWidth`). */
.catwrap--crew { flex-wrap: nowrap; }

/* Servizi / mezzi: tinted text pills (bronze / blue) */
.tpill { display: inline-flex; align-items: center; gap: .3rem; height: 22px; padding: 0 .5rem; border-radius: var(--qc-radius-pill); font: 600 var(--qc-fs-xs) var(--qc-font-body); white-space: nowrap; }
.tpill svg { width: 12px; height: 12px; flex: none; pointer-events: none; }
.tpill--serv { color: color-mix(in srgb, var(--qc-accent) 62%, var(--qc-text-primary)); background: color-mix(in srgb, var(--qc-accent) 16%, var(--qc-surface)); border: 1px solid color-mix(in srgb, var(--qc-accent) 34%, transparent); }
.tpill--mezzi { color: color-mix(in srgb, var(--qc-info) 58%, var(--qc-text-primary)); background: color-mix(in srgb, var(--qc-info) 16%, var(--qc-surface)); border: 1px solid color-mix(in srgb, var(--qc-info) 36%, transparent); }
/* Commessa: the agreement a job belongs to (customer sheet). Violet, never the
   bronze accent, which is brand and not meaning. A link where the reader may
   open the agreement. */
.tpill--commessa { color: color-mix(in srgb, var(--qc-status-purple) 55%, var(--qc-text-primary)); background: color-mix(in srgb, var(--qc-status-purple) 18%, var(--qc-surface)); border: 1px solid color-mix(in srgb, var(--qc-status-purple) 40%, transparent); text-decoration: none; }
.tpill--commessa .ti { font-size: 12px; }
a.tpill--commessa:hover, a.tpill--commessa:focus-visible { border-color: var(--qc-status-purple); text-decoration: underline; }
/* A pill can be asked to give up width so the "+N" fits beside it (see fitPills).
   The icon keeps its size — it is what says whether this is a service or a
   vehicle — and the name takes the ellipsis, having its full form a press away. */
.catwrap .tpill { overflow: hidden; max-width: 100%; }
.catwrap .tpill > span { overflow: hidden; text-overflow: ellipsis; min-width: 0; }

/* Pills the cell has no room for. `display: none` rather than hidden-but-there:
   they must stop taking part in the wrapping, or the ones still shown would be
   laid out around gaps left by their absent neighbours. */
.tpill.is-over { display: none; }
/* The counter that stands for them. Neutral, so it reads as a count of the two
   kinds beside it rather than as a third kind of its own. */
.tpill--more {
  cursor: pointer;
  color: var(--qc-text-secondary);
  background: var(--qc-surface-light);
  border: 1px dashed var(--qc-border);
  font-variant-numeric: tabular-nums;
}
.tpill--more:hover { color: var(--qc-text-primary); border-color: var(--qc-primary); }
.tpill--more:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 1px; }

/* ============================================================================
   Work status bar — one chip per status, tinted in that status' own colour.
   Markup: templates/work/_status_tabs.html.twig.

   Each chip carries its accent as --qc-chip-accent, set inline from the
   Work::WORK_*_COLOR constants via the --qc-status-* tokens.

   The label always sits on a text token, never on a saturated fill: three of
   the six status hues (lime, cyan, yellow) are light, and text on top of them
   at full strength misses 4.5:1. Tinting the ground instead keeps every chip
   legible in both themes while the hue still identifies the status.
   ============================================================================ */
.qc-statusbar {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
}
.qc-statusbar__list {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
    flex: 1 1 auto;
    min-width: 0;
    list-style: none;
    margin: 0;
    padding: 0;
}
.qc-statuschip {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    height: 34px;
    padding: 0 .75rem;
    border-radius: var(--qc-radius-pill);
    border: 1px solid color-mix(in srgb, var(--qc-chip-accent) 30%, transparent);
    background: color-mix(in srgb, var(--qc-chip-accent) 12%, transparent);
    color: var(--qc-text-secondary);
    font: 600 var(--qc-fs-sm)/1 var(--qc-font-body);
    white-space: nowrap;
    text-decoration: none;
    transition: background var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease),
                color var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease);
}
.qc-statuschip:hover {
    background: color-mix(in srgb, var(--qc-chip-accent) 20%, transparent);
    color: var(--qc-text-primary);
    text-decoration: none;
}
/* Active ring drawn as an inset shadow rather than a thicker border, so the
   chip keeps the exact height and width it had while inactive.

   `.active` alongside `.is-active` because a chip bar is not always a set of
   links: on the management-items page the chips switch tab panes on the spot,
   and Bootstrap's tab plugin marks the current one with its own class. */
.qc-statuschip.is-active,
.qc-statuschip.active {
    background: color-mix(in srgb, var(--qc-chip-accent) 22%, var(--qc-surface));
    color: var(--qc-text-primary);
    box-shadow: inset 0 0 0 2px var(--qc-chip-accent), var(--qc-shadow-sm);
}
.qc-statuschip:focus-visible {
    outline: 2px solid var(--qc-primary);
    outline-offset: 2px;
}
.qc-statuschip__dot {
    width: .55rem;
    height: .55rem;
    flex: none;
    border-radius: var(--qc-radius-pill);
    background: var(--qc-chip-accent);
}
.qc-statuschip__count {
    font-weight: 700;
    color: var(--qc-text-tertiary);
    font-variant-numeric: tabular-nums;
}
.qc-statuschip.is-active .qc-statuschip__count,
.qc-statuschip.active .qc-statuschip__count { color: var(--qc-text-primary); }

/* Setup link — outside the chip list, pushed to the far end of the bar. */
.qc-statusbar__setup {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    flex: none;
    height: 34px;
    padding: 0 .6rem;
    border-radius: var(--qc-radius-btn);
    color: var(--qc-text-secondary);
    font: 600 var(--qc-fs-sm)/1 var(--qc-font-body);
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease),
                color var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease);
}
.qc-statusbar__setup:hover {
    background: var(--qc-layer-selected);
    color: var(--qc-text-primary);
    text-decoration: none;
}
.qc-statusbar__setup.is-active {
    background: var(--qc-layer-wash);
    color: var(--qc-primary);
}
.qc-statusbar__setup:focus-visible {
    outline: 2px solid var(--qc-primary);
    outline-offset: 2px;
}
.qc-statusbar__setup .icon { width: 16px; height: 16px; }

/* Narrow screens: the chips already wrap, so only the setup label goes —
   the gear alone still reads, and the chips keep the full width. */
@media (max-width: 575.98px) {
    .qc-statusbar__setup-label { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .qc-statuschip,
    .qc-statusbar__setup { transition: none; }
}

/* ---- address suggestions ---------------------------------------------------
   The street, the town and the house number offer what is being typed into
   them. The list hangs off the field itself, absolutely placed so it covers
   whatever is under it instead of pushing the row apart mid-keystroke.
   Assembled by assets/controllers/address_autocomplete_controller.js. */
.qc-suggest-anchor { position: relative; }
.qc-suggest {
    position: absolute;
    top: calc(100% - 2px); left: 0; right: 0;
    z-index: 30;
    max-height: 16rem;
    overflow-y: auto;
    display: flex; flex-direction: column;
    background: var(--qc-surface-overlay);
    border: 1px solid var(--qc-edge);
    border-radius: var(--qc-radius-input);
    box-shadow: var(--qc-shadow-lg), var(--qc-shadow-top);
}
.qc-suggest[hidden] { display: none; }
.qc-suggest__item {
    display: flex; align-items: baseline; gap: 12px;
    width: 100%;
    padding: 9px 12px;
    border: 0; border-bottom: 1px solid var(--qc-divider);
    background: transparent;
    color: var(--qc-text-primary);
    font-family: inherit; font-size: var(--qc-fs-lg); text-align: left;
    cursor: pointer;
    transition: background var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease);
}
.qc-suggest__item:last-child { border-bottom: 0; }
/* The same wash the inputs use for :focus, so "the one I am on" reads the same
   way whether it was reached by mouse or by arrow key. */
.qc-suggest__item:hover,
.qc-suggest__item.is-active {
    background: var(--qc-layer-selected);
}
.qc-suggest__item:focus-visible {
    outline: 2px solid var(--qc-primary);
    outline-offset: -2px;
}
.qc-suggest__name { flex: 1; min-width: 0; }
.qc-suggest__meta {
    color: var(--qc-text-secondary);
    white-space: nowrap;
    font-size: var(--qc-fs-md);
}
.qc-suggest__empty,
.qc-suggest__loading {
    padding: 9px 12px;
    color: var(--qc-text-secondary);
    font-size: var(--qc-fs-md);
}
.qc-suggest__empty[hidden],
.qc-suggest__loading[hidden] { display: none; }
/* The ring loader carries its own stacked layout; in a one-line strip it sits
   beside its label instead. */
.qc-suggest__loading .qc-loader { flex-direction: row; align-items: center; gap: .5rem; }

@media (prefers-reduced-motion: reduce) {
    .qc-suggest__item { transition: none; }
}

/* ---- the sheet's sites, as an accordion -----------------------------------
   One row per place (form/_customer_address_theme.html.twig): a button that
   names it, and — open — its fields with the row's acts at the foot. One open
   at a time (customer_addresses_controller.js).

   Each site is a box of its own, a small gap apart. They were hairline rows in
   one frame, and on the dark card the hairline (1.12:1) left nothing between
   one site and the next. The box's edge is halfway between the hairline and a
   field's outline — a site is a container, not a control — and the open one
   takes the primary tint, so which site the fields below belong to is never
   in doubt. */
.qc-addresses [data-customer-addresses-target="collection"] {
    display: grid;
    gap: .5rem;
}
.qc-addresses [data-customer-addresses-target="collection"]:empty { display: none; }
.qc-address-item {
    border: 1px solid color-mix(in srgb, var(--qc-field-border) 55%, var(--qc-border));
    border-radius: var(--qc-radius-input);
    background: var(--qc-card-bg);
    overflow: hidden;
    transition: border-color var(--qc-dur-standard) var(--qc-ease-standard);
}
.qc-address-item:hover { border-color: var(--qc-field-border); }
.qc-address-item.is-open {
    border-color: color-mix(in srgb, var(--qc-primary) 55%, var(--qc-field-border));
    box-shadow: var(--qc-shadow-sm);
}
.qc-address-row {
    display: flex;
    align-items: center;
    gap: .75rem;
    width: 100%;
    padding: .875rem 1rem;
    border: 0;
    background: transparent;
    color: var(--qc-text-primary);
    text-align: left;
    cursor: pointer;
    transition: background-color var(--qc-dur-standard) var(--qc-ease-standard);
}
.qc-address-row:hover { background: var(--qc-layer-hover); }
.qc-address-row:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: -2px; }
.qc-address-item.is-open .qc-address-row { background: var(--qc-layer-hover); }
.qc-address-row__pin { display: inline-flex; font-size: var(--qc-fs-2xl); color: var(--qc-text-tertiary); }
.qc-address-row__pin[data-state="found"] { color: var(--qc-success-text); }
.qc-address-row__text { display: grid; min-width: 0; flex: 1 1 auto; }
.qc-address-row__title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qc-address-row__line { font-size: var(--qc-fs-md); color: var(--qc-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qc-address-item.is-retired .qc-address-row__title { color: var(--qc-text-secondary); }
.qc-address-row__pills { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .375rem; }
.qc-address-row__pills [hidden] { display: none; }
.qc-address-row__chevron {
    color: var(--qc-text-tertiary);
    transition: transform var(--qc-dur-standard) var(--qc-ease-standard);
}
.qc-address-item.is-open .qc-address-row__chevron { transform: rotate(180deg); }
.qc-address-body { padding: 1rem; border-top: 1px solid var(--qc-divider); }
.qc-address-body[hidden] { display: none; }
.qc-address-body > * + * { margin-top: 1rem; }
.qc-address-body__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    padding-top: 1rem;
    border-top: 1px solid var(--qc-divider);
}
.qc-address-body__foot .qc-form__row { margin: 0; }
.qc-address-body__acts { display: flex; gap: .5rem; margin-left: auto; }
.qc-address-body__acts [hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
    .qc-address-row, .qc-address-row__chevron, .qc-address-item { transition: none; }
}

/* ---- address verdict -------------------------------------------------------
   Where the map puts the place: one pin at the right end of the place block's
   first line, above the labels, so it reads as being about the whole address
   rather than the house number beside it. Four states and a
   colour each — grey idle, rings checking, green found, amber missing — with
   the words as its tooltip (templates/customer/_address_place.html.twig).
   Written by assets/controllers/address_autocomplete_controller.js. */
/* The block's first line: here or abroad on the left, the pin on the right. */
.qc-address-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .75rem;
}
.qc-address-top > .qc-address-verdict {
    display: flex;
    align-items: center;
    height: 1.75rem;
    line-height: 1;
}
.qc-address-verdict [data-verdict] { border-radius: var(--qc-radius-pill); }
.qc-address-verdict [data-verdict]:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }
.qc-address-verdict .ti { font-size: 1.375rem; }
.qc-address-verdict [data-verdict] { display: inline-flex; }
.qc-address-verdict [data-verdict][hidden] { display: none; }
.qc-address-verdict[data-state="idle"] { color: var(--qc-text-tertiary); }
.qc-address-verdict[data-state="found"] a { color: var(--qc-success-text); text-decoration: none; }
/* The amber that type is set in, and not the amber fill: the fill is 1.5:1 on
   the light card, under the 3:1 an icon needs. On dark it is the yellow. */
.qc-address-verdict[data-state="missing"] { color: var(--qc-warning-text); }
.qc-address-verdict .qc-loader { flex-direction: row; align-items: center; padding: 0; }

/* ==================================================================
 * §5.4b — The two-pane shell: a rail of subjects, and one subject
 *
 * The settings invented it. They were four pages in four parts of the app, two
 * of them reachable only from a small gear on a card, and the rail is the
 * answer to "where is that setting" — the same on every section, reusing the
 * sidebar recipe above rather than inventing a second navigation language,
 * accent marker included, which stays the only bronze in the chrome.
 *
 * It carried the settings' name until three other pages needed the same shape:
 * the help centre, the account, and the user sheet — where it replaced a strip
 * of tabs that claimed three panes of one form were three forms. So the names
 * are the brand's now (`.qc-shell`, `.qc-sidenav`) and what is still called
 * `.qc-settings__*` further down is content only the settings hub has: its
 * logo, its link rows, its counters.
 *
 * The rail is drawn once, by templates/components/_side_nav.html.twig, in
 * either of two modes — a list of addresses, or a set of panes of one form.
 * ================================================================== */
.qc-shell {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

/* `--wide`: a shell whose pane holds a data grid rather than a form.
 *
 * The rail costs 15rem and its gutter — about 264px — and that is free on a
 * page of fields, where the column is capped at 42rem however much room there
 * is. It is not free on a grid: at 1180 the commission's grids came back with
 * their headers truncated to "PR…" and the row's own action buttons sliced off
 * at the card's edge.
 *
 * So the same rail, and the same routes, but it steps out of the way sooner —
 * above the content and laid out along the line, rather than beside it. The
 * plain shell keeps stacking at the phone breakpoint further down, because a
 * form has nothing to gain from the extra width. */
@media (max-width: 1200px) {
  .qc-shell--wide { grid-template-columns: minmax(0, 1fr); }
  .qc-shell--wide .qc-sidenav { position: static; }
  .qc-shell--wide .qc-sidenav__list {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
  }
  .qc-shell--wide .qc-sidenav__item { width: auto; }
  /* The active marker is a bar on the left edge, which reads as "this one, in
     the list going down". Along a line it would sit under the label before it. */
  .qc-shell--wide .qc-sidenav__item.is-active::before { display: none; }
}

.qc-sidenav {
  position: sticky;
  top: 1rem;
  background: var(--qc-surface-light);
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-card);
  padding: .5rem;
}

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

.qc-sidenav__item {
  display: flex;
  align-items: center;
  gap: .5rem;
  position: relative;
  /* In `panes` mode an entry is a <button>, which arrives with the browser's
     own chrome: a border, a grey face, a smaller font, and a width that shrinks
     to its label. Reset it here rather than in a second rule, so an entry that
     is a link and an entry that is a button cannot come to look like two
     different kinds of thing. */
  width: 100%;
  text-align: left;
  font: inherit;
  border: 0;
  background: none;
  padding: .5rem .75rem;
  margin: 2px 0;
  border-radius: var(--qc-radius-btn);
  color: var(--qc-text-secondary);
  text-decoration: none;
  transition: background-color var(--qc-dur-fast) var(--qc-ease-fast),
              color var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-sidenav__item:hover {
  background-color: var(--qc-layer-hover);
  color: var(--qc-text-primary);
}
/* Where you are, as the main menu says it (§5.4): the one entry lifted off the
   rail, with the bronze dot. */
.qc-sidenav__item.is-active {
  background: var(--qc-surface-base);
  box-shadow: 0 0 0 1px var(--qc-divider), var(--qc-shadow-sm);
  color: var(--qc-text-primary);
  font-weight: 600;
}
/* The entry's icon: quiet beside the label, the primary once it is pointed at
   or is where you are. */
.qc-sidenav__icon {
  flex: none;
  font-size: 1.125rem;
  color: var(--qc-text-tertiary);
  transition: color var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-sidenav__item:hover .qc-sidenav__icon,
.qc-sidenav__item.is-active .qc-sidenav__icon { color: var(--qc-primary); }
.qc-sidenav__item.is-active::before {
  content: "";
  position: absolute;
  left: -.3rem; top: 50%; transform: translateY(-50%);
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--qc-accent);
}

.qc-sidenav__aside {
  border-top: 1px solid var(--qc-divider);
  margin-top: .5rem;
  padding-top: .5rem;
  display: flex;
  flex-direction: column;
}
.qc-sidenav__link {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: .35rem .75rem;
  color: var(--qc-text-tertiary);
  text-decoration: none;
  font-size: var(--qc-fs-md);
}
/* The mark of a link that opens in a new tab: the link's own colour. */
.qc-sidenav__ext { font-size: .875em; }
.qc-sidenav__link:hover { color: var(--qc-text-primary); }

/* The pane's opening line: what this section is about on the left, what you can
   do in it on the right.

   The actions are here rather than in the strip under the bar
   (`.qc-pageactions`, topbar.css) because that strip is full width and this page
   is two columns: it would push the rail down, and only on the sections that
   happen to carry a button — so the rail moved as you clicked along it. A page
   that takes its actions this way says so with `actions_in_pane`.

   It wraps: on a phone the rail is already a horizontal strip and the buttons
   drop under the sentence rather than squeezing it. */
.qc-shell__intro {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .75rem 1rem;
  margin-bottom: 1rem;
}
/* Always at the far end, whether or not there is a sentence to its left:
   `space-between` alone would leave a lone group of buttons at the start, and a
   page's actions belong in the corner on every page that has them. */
.qc-shell__actions {
  flex: 0 0 auto;
  margin-left: auto;
}

/* `flex` here is what keeps the sentence on the buttons' line and makes it the
   one that gives way: it may shrink to 18rem before they wrap. */
.qc-shell__lede {
  flex: 1 1 18rem;
  color: var(--qc-text-secondary);
  margin: 0;
}

.qc-settings__linkrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.qc-settings__linktitle { font-family: var(--qc-font-display); font-size: var(--qc-fs-xl); margin: 0 0 .25rem; }
.qc-settings__linklede { color: var(--qc-text-secondary); margin: 0; }

.qc-settings__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 1rem;
}
.qc-settings__statvalue { font-size: var(--qc-fs-2xl); font-weight: 700; color: var(--qc-text-primary); }
.qc-settings__statlabel { color: var(--qc-text-secondary); font-size: var(--qc-fs-md); }

/* A share of the whole, as a bar. Its own element rather than a progress bar so
   the fill is a token and not a framework colour. */
.qc-settings__bar {
  height: .5rem;
  border-radius: 999px;
  background: var(--qc-layer-selected);
  overflow: hidden;
}
.qc-settings__bar::after {
  content: "";
  display: block;
  height: 100%;
  width: var(--qc-bar, 0%);
  border-radius: inherit;
  background: var(--qc-primary);
}
/* A seat count past the licence: full, and in the warning colour — said, not
   refused (docs/license.md). Thin under a stat card's figure. */
.qc-settings__bar.is-over::after { background: var(--qc-warning); }
.qc-settings__bar--thin { height: .25rem; }

/* The licence: the edition's name is a title, and the three editions are a
   ladder — the one held outlined in primary, the ones above it in secondary
   ink (never dimmed with opacity). */
.qc-license__edition { font-family: var(--qc-font-display); }
.qc-license__ladder {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem;
}
.qc-license__tier {
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-btn);
  padding: 1rem;
  background: var(--qc-surface-light);
}
.qc-license__tier.is-current {
  border-color: var(--qc-primary);
  box-shadow: inset 0 0 0 1px var(--qc-primary);
}
.qc-license__tierhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin-bottom: .25rem;
}
.qc-license__tiername {
  font-family: var(--qc-font-display);
  font-size: var(--qc-fs-xl);
  font-weight: 700;
  margin: 0;
}
.qc-license__tierlede { color: var(--qc-text-secondary); font-size: var(--qc-fs-md); margin: 0 0 .75rem; }
.qc-license__tierplus {
  font-size: var(--qc-fs-xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--qc-text-secondary);
  margin: 0 0 .5rem;
}
.qc-license__features { list-style: none; padding: 0; margin: 0; }
.qc-license__features li { display: flex; gap: .5rem; align-items: baseline; padding: .15rem 0; }
.qc-license__features .ti { color: var(--qc-primary); flex: none; }
.qc-license__tier.is-beyond .qc-license__features,
.qc-license__tier.is-beyond .qc-license__tiername,
.qc-license__tier.is-beyond .qc-license__features .ti { color: var(--qc-text-secondary); }

/* Below lg the rail becomes a scrollable row above the section, the same shape
   the work list's status bar already uses. */
@media (max-width: 991.98px) {
  .qc-shell { grid-template-columns: minmax(0, 1fr); }
  .qc-sidenav { position: static; }
  .qc-sidenav__list { display: flex; gap: .25rem; overflow-x: auto; }
  .qc-sidenav__item { white-space: nowrap; }
  .qc-sidenav__item.is-active::before { display: none; }
  .qc-sidenav__aside { flex-direction: row; gap: 1rem; }
}

/* ==================================================================
   The clock on the stamping page
   ==================================================================
   It read `bg-light text-primary` with an inline font-size. `bg-light` is a
   Bootstrap utility the token layer does not remap, so the block stayed pale
   grey in dark theme with primary-green text on it — the one element on the
   page that did not follow the reader's choice. Tokens instead, and the size
   in the stylesheet where a size belongs. */
.qc-clock {
  background: var(--qc-surface-light);
  border: 1px solid var(--qc-border);
  color: var(--qc-text-primary);
  font-size: var(--qc-fs-2xl);
  font-weight: 600;
  /* The seconds tick once a second, and proportional digits make the whole
     line shuffle sideways every time they do. */
  font-variant-numeric: tabular-nums;
  /* First paint has no time in it yet: hold the line's height so the button
     below does not jump when the controller connects. */
  min-height: calc(1.25rem * 1.4 + 1rem);
}

/* The type of stamping, above the one button that makes it. Shown only when
   there is a choice to make — the works module on, and something of this
   operator's scheduled today. */
.qc-punch__pick { margin-bottom: .85rem; }
.qc-punch__picklabel {
  display: block;
  font-size: var(--qc-fs-sm);
  font-weight: 600;
  color: var(--qc-text-secondary);
  margin-bottom: .4rem;
  text-align: left;
}
.qc-punch__pick .qc-seg { width: 100%; }
.qc-punch__pick .qc-seg__btn { flex: 1; }

/* ==================================================================
   The pivot
   ==================================================================
   A matrix, not a list of records — the one table in the application that is
   not an AG Grid, because its rows are crossed cells and not records. It reads
   like the grids all the same: the same header (surface-light, uppercase,
   tracked), the same rules, the same hover and the same zebra, all off the
   --qc-grid-* tokens, so it follows the account's density with them rather
   than staying comfortable while every list around it goes compact.

   The heading row and the first column stay put while the rest scrolls: with
   three months of columns, losing sight of whose row you are on is the
   difference between reading the table and guessing at it. */
.qc-pivot {
  overflow: auto;
  max-height: 32rem;
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-card, 16px);
}
.qc-pivot__table {
  margin: 0;
  border-collapse: separate;
  border-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.qc-pivot__table th,
.qc-pivot__table td {
  background: var(--qc-surface);
  border-bottom: 1px solid var(--qc-divider);
  padding: .45rem var(--qc-grid-cell-px);
  font-size: var(--qc-grid-data-font-size);
  white-space: nowrap;
  text-align: right;
  vertical-align: middle;
}
.qc-pivot__table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  height: var(--qc-grid-header-h);
  background: var(--qc-surface-light);
  color: var(--qc-text-secondary);
  font-size: var(--qc-grid-header-font-size);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
}
/* Zebra and hover are painted as a layer over the cell rather than as its
   background: the row head and the two total columns carry backgrounds of
   their own — and have to, because they are sticky and the rest of the row
   scrolls underneath them — so a background alone would leave three cells of
   every row unstriped and unlit. */
.qc-pivot__table tbody tr:nth-child(even) > * {
  background-image: linear-gradient(var(--qc-grid-row-odd), var(--qc-grid-row-odd));
}
.qc-pivot__table tbody tr:hover > * {
  background-image: linear-gradient(var(--qc-grid-row-hover), var(--qc-grid-row-hover));
}
/* The job's own column: sticky sideways, and the one cell in the row that
   reads as words rather than figures. */
.qc-pivot__rowhead {
  position: sticky;
  left: 0;
  z-index: 3;
  text-align: left !important;
  white-space: normal;
  min-width: 15rem;
  max-width: 15rem;
  border-right: 1px solid var(--qc-border);
  color: var(--qc-text-primary);
  font-weight: 600;
}
.qc-pivot__table thead .qc-pivot__rowhead { z-index: 4; }
.qc-pivot__sub {
  display: block;
  font-weight: 400;
  font-size: var(--qc-grid-cell-small-size);
  color: var(--qc-text-secondary);
}
.qc-pivot__total {
  background: color-mix(in srgb, var(--qc-primary) 7%, var(--qc-surface)) !important;
  font-weight: 700;
  color: var(--qc-text-primary);
}
.qc-pivot__empty { color: var(--qc-text-tertiary, var(--qc-text-secondary)); }

/* The monthly attendance pivot: the same component, with a day per column.
   Fixed widths in CSS rather than measured in JavaScript — the controller used
   to read offsetWidth off every cell and write the largest back inline, a
   layout pass per cell on a table that carries up to ninety of them. */
.qc-pivot--days {
  /* Three widths, declared once. The columns are laid out from them and the
     sticky offsets are computed from them, so the two can never disagree —
     and when they disagreed the table showed it plainly: a pinned column
     parked past the end of the name left an empty strip down the middle. */
  --qc-pivot-name-w: 12rem;
  --qc-pivot-total-w: 8rem;
  --qc-pivot-day-w: 6.5rem;
}
.qc-pivot--days .qc-pivot__table { table-layout: fixed; }
/* A day column is six characters wide and does not need a grid cell's side
   padding; the room goes to the heading, which in Polish is one long word. */
.qc-pivot--days .qc-pivot__table th,
.qc-pivot--days .qc-pivot__table td { width: var(--qc-pivot-day-w); padding-inline: .5rem; }
/* The name and the two totals are wider than a day, and say so from inside the
   same table — a class on its own would lose to the rule above, which carries
   an element selector, and the operator column would come out as narrow as a
   Tuesday. Which is what happened: with a month on screen the table is wider
   than the page, there is no slack left to hand back, and every name was
   squeezed into six characters' worth of column. */
.qc-pivot--days .qc-pivot__table .qc-pivot__rowhead {
  width: var(--qc-pivot-name-w);
  min-width: var(--qc-pivot-name-w);
  max-width: var(--qc-pivot-name-w);
  padding-inline: var(--qc-grid-cell-px);
}
.qc-pivot--days .qc-pivot__table .qc-pivot__total {
  width: var(--qc-pivot-total-w);
  padding-inline: .625rem;
}
/* Where the summary ends and the days begin. On the second total rather than
   on the first day, because that edge is the one that stays: the day cell and
   its border scroll away under the pinned columns. */
.qc-pivot--days .qc-pivot__table tr > .qc-pivot__total:nth-child(3) {
  border-right: 1px solid var(--qc-border);
}
/* The two summary columns travel with the name. On three months of columns the
   figure the row exists for would otherwise be ninety columns behind the
   reader — and a total that has to be scrolled back to is not a total.
   Not on a phone: the name alone is already half that screen. */
@media (min-width: 768px) {
  .qc-pivot--days .qc-pivot__table tr > .qc-pivot__total:nth-child(2) {
    position: sticky;
    left: var(--qc-pivot-name-w);
    z-index: 3;
  }
  .qc-pivot--days .qc-pivot__table tr > .qc-pivot__total:nth-child(3) {
    position: sticky;
    left: calc(var(--qc-pivot-name-w) + var(--qc-pivot-total-w));
    z-index: 3;
  }
  .qc-pivot--days .qc-pivot__table thead tr > .qc-pivot__total { z-index: 4; }
}
/* The heading wraps rather than running over its neighbour: the columns are a
   fixed width, so a month name that does not fit has nowhere else to go. */
.qc-pivot--days .qc-pivot__table thead th { text-align: center; line-height: 1.25; white-space: normal; }
.qc-pivot--days .qc-pivot__table tbody td { text-align: center; }
.qc-pivot--days .qc-pivot__rowhead a { text-decoration: none; color: inherit; }
.qc-pivot--days .qc-pivot__rowhead a:hover { color: var(--qc-primary); }
/* Saturday and Sunday, told apart at a glance. The column already says which
   day it is; the tint only saves the eye from reading thirty headings to find
   where a week ends, which is why it is faint and carries nothing of its own. */
.qc-pivot--days tbody .qc-pivot__weekend {
  background-color: color-mix(in srgb, var(--qc-surface-light) 70%, var(--qc-surface));
}
.qc-pivot--days thead .qc-pivot__weekend { color: var(--qc-text-primary); }

/* Absence and permission read as words first. The tint only confirms what the
   word already says, which is why neither is a bare colour. */
.qc-pivot__mark {
  display: block;
  margin-top: .15rem;
  font-size: .75rem;
  font-weight: 600;
  border-radius: var(--qc-radius-sm, 6px);
  padding: .05rem .35rem;
  white-space: nowrap;
}
.qc-pivot__mark--absence {
  background: color-mix(in srgb, var(--qc-error) 16%, transparent);
  color: var(--qc-error-text, var(--qc-error));
}
.qc-pivot__mark--permission {
  background: color-mix(in srgb, var(--qc-warning) 22%, transparent);
  color: var(--qc-warning-text, var(--qc-text-primary));
}

/* The month: a column per day, thirty-one of them.
   Narrow columns, because what a cell says is a figure ("8:10") or a mark two
   letters wide; the name and the hours stay put while the days scroll, and the
   day totals are held under the list the way the heading is held above it. */
.qc-pivot--month {
  --qc-pivot-name-w: 13rem;
  --qc-pivot-total-w: 4.75rem;
  --qc-pivot-day-w: 4.25rem;
  max-height: min(72vh, 46rem);
}
.qc-pivot--month .qc-pivot__table { table-layout: fixed; }
.qc-pivot--month .qc-pivot__table th,
.qc-pivot--month .qc-pivot__table td {
  width: var(--qc-pivot-day-w);
  padding: .35rem .25rem;
  text-align: center;
}
/* A weekday and a number: not shouted. The uppercase heading of the other
   pivots is three letters too wide for a column this narrow. */
.qc-pivot--month .qc-pivot__table thead th {
  line-height: 1.25;
  white-space: normal;
  text-transform: none;
  letter-spacing: 0;
}
.qc-pivot--month .qc-pivot__table thead abbr { text-decoration: none; cursor: help; }
.qc-pivot--month .qc-pivot__table .qc-pivot__rowhead {
  width: var(--qc-pivot-name-w);
  min-width: var(--qc-pivot-name-w);
  max-width: var(--qc-pivot-name-w);
  padding-inline: var(--qc-grid-cell-px);
}
.qc-pivot--month .qc-pivot__table .qc-pivot__total { width: var(--qc-pivot-total-w); }
.qc-pivot--month .qc-pivot__rowhead a { text-decoration: none; color: inherit; }
.qc-pivot--month .qc-pivot__rowhead a:hover { color: var(--qc-primary); }
/* The hours stay beside the name; the reasons' columns scroll with the days.
   Not on a phone: the name alone is already half that screen. */
@media (min-width: 768px) {
  .qc-pivot--month .qc-pivot__table tr > .qc-pivot__total:nth-child(2) {
    position: sticky;
    left: var(--qc-pivot-name-w);
    z-index: 3;
    border-right: 1px solid var(--qc-border);
  }
  .qc-pivot--month .qc-pivot__table thead tr > .qc-pivot__total:nth-child(2),
  .qc-pivot--month .qc-pivot__table tfoot tr > .qc-pivot__total:nth-child(2) { z-index: 5; }
}
/* A day nobody is expected to work: Saturday, Sunday, a public holiday. */
.qc-pivot--month tbody .qc-pivot__weekend,
.qc-pivot--month tfoot .qc-pivot__weekend {
  background-color: color-mix(in srgb, var(--qc-surface-light) 70%, var(--qc-surface));
}
.qc-pivot--month thead .qc-pivot__weekend { color: var(--qc-text-primary); }
/* The totals of each day, held at the foot as the heading is held at the top. */
.qc-pivot--month .qc-pivot__table tfoot th,
.qc-pivot--month .qc-pivot__table tfoot td {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--qc-surface-light);
  border-top: 1px solid var(--qc-border);
  border-bottom: 0;
  font-weight: 700;
  color: var(--qc-text-primary);
}
.qc-pivot--month .qc-pivot__table tfoot .qc-pivot__rowhead { z-index: 5; }
/* A cell is a link to that day on the person's own sheet: the whole cell, so
   the target is the size of the cell and not of the figure in it. */
.qc-pivot__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .2rem;
  min-height: 1.75rem;
  color: inherit;
  text-decoration: none;
  border-radius: var(--qc-radius-sm, 4px);
}
.qc-pivot__cell:hover { color: var(--qc-primary); }
.qc-pivot__cell:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }
/* The same pill, with the room a matrix cell has. */
.qc-stpill.qc-stpill--cell { padding: 1px 6px 1px 5px; gap: 3px; }
/* A day off on a person's sheet: the tint the matrix gives its column. */
.qc-grid__grid .ag-row.qc-row-off { background-color: color-mix(in srgb, var(--qc-surface-light) 55%, transparent); }

/* ==========================================================================
   Moved out of dashboard.css when the calendar left the home: these three
   families are drawn away from that page and would have gone dark with it.
   .qc-worklist* — ag_grid_controller.js on every grid, and the today-jobs
   fragment; .qc-act* — the grid's `actions` renderer; .qc-qr/.qc-code/
   .qc-app-icon — app/_connection.html.twig, which the person sheet includes.
   ========================================================================== */

/* "Giornata" modal: today's jobs (time · customer · status). */
.qc-worklist { list-style: none; margin: 0; padding: 0; }
.qc-worklist__item { display: flex; align-items: center; gap: .65rem; padding: .5rem .25rem; }
/* Divider on the list items, not on the rows themselves: a row is a link now and
   sits inside its <li>, so the rows are no longer each other's siblings. */
.qc-worklist > li + li { border-top: 1px solid var(--qc-divider); }
/* A row opens the job it names. It stays a row rather than looking like a link —
   no underline, no link colour, the state chip carrying the only colour — and
   answers to the pointer with a tint, which is what says it can be pressed. */
a.qc-worklist__item {
  color: inherit;
  text-decoration: none;
  border-radius: var(--qc-radius-btn);
  transition: background var(--qc-dur-fast) var(--qc-ease-fast);
}
a.qc-worklist__item:hover { background: var(--qc-layer-hover); }
a.qc-worklist__item:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: -2px; }
/* Phone: a row is something to hit with a thumb, so it gets the height for one.
   .5rem of padding makes about 36px, which is under any comfortable target. */
@media (max-width: 575.98px) {
  .qc-worklist__item { min-height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
  a.qc-worklist__item { transition: none; }
}
.qc-worklist__time { flex: none; font-weight: 700; color: var(--qc-text-primary); }
.qc-worklist__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--qc-text-primary); }
/* The status a row shows is `.qc-stpill` (§5.6) — the same object and the same
   recipe as the one on the Giornata. `.qc-worklist__state` was a fourth
   spelling of it, at a fourth tint and half a pixel short of the floor. */
.qc-worklist .qc-stpill { flex: none; }

/* ---- App / QR framed box (stays light for scanner) ---- */
.qc-qr { display: inline-block; padding: 14px; border-radius: 16px; background: #fff; border: 1px solid var(--qc-border); box-shadow: var(--qc-shadow-sm); line-height: 0; }
.qc-code { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .5rem .7rem; border-radius: 10px; background: var(--qc-field-bg); border: 1px solid var(--qc-border); }
.qc-code code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--qc-fs-sm); color: var(--qc-text-primary); }

/* Official store badges. Third-party artwork that neither owner allows recolouring,
   so it keeps a light plate under it in either theme — the same exception the QR
   frame takes. Apple asks for at least 40px of badge height onscreen, Google for
   28px, and both for a quarter of the height as clear space, which is the padding.
   Play's artwork is framed with padding baked in, hence the taller rule: that is
   the one number to retune if a redownloaded file frames it differently. */
.qc-badge { display: inline-flex; align-items: center; padding: 10px; border-radius: 12px; background: #fff; border: 1px solid var(--qc-border); box-shadow: var(--qc-shadow-sm); line-height: 0; }
.qc-badge img { display: block; height: 40px; width: auto; }
.qc-badge--play img { height: 54px; }
/* The app's own icon beside them, at the height of a badge plate. Rounded here
   because the artwork is square: a store draws that mask itself. */
.qc-app-icon { display: block; width: 62px; height: 62px; border-radius: 22.5%; }

/* ---- Putting the app on a phone, in three steps (app/_connection) ----------
   Download, scan, sign in — numbered because the order is the instruction,
   one under the other with a hairline joining the numbers. The same at the
   width of the person's sheet and of the drawer: the QR sits beside its
   sentence where there is room and above it where there is not. The lines to
   type by hand are a disclosure under the steps.
   ------------------------------------------------------------------------- */
.qc-appsteps { display: flex; flex-direction: column; gap: 1rem; max-width: 34rem; text-align: left; }
.qc-appsteps__list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
.qc-appsteps__step { position: relative; display: grid; grid-template-columns: 1.875rem minmax(0, 1fr); gap: .25rem .875rem; padding-bottom: 1.125rem; }
.qc-appsteps__step:last-child { padding-bottom: 0; }
.qc-appsteps__step:not(:last-child)::before { content: ""; position: absolute; left: calc(.9375rem - .5px); top: 2.125rem; bottom: .25rem; width: 1px; background: var(--qc-border); }
.qc-appsteps__n { display: grid; place-items: center; width: 1.875rem; height: 1.875rem; border-radius: 50%; border: 1.5px solid var(--qc-primary); font-size: var(--qc-fs-md); font-weight: 600; color: var(--qc-primary); }
.qc-appsteps__title { align-self: center; font-family: var(--qc-font-display); font-size: var(--qc-fs-lg); font-weight: 600; line-height: 1.3; color: var(--qc-text-primary); }
.qc-appsteps__body { grid-column: 2; display: flex; flex-direction: column; gap: .625rem; min-width: 0; }
/* The icon, then the two stores as one group. The row itself never wraps: in
   the drawer the pair folds onto two lines beside the icon, rather than
   leaving one badge next to it and the other alone underneath. */
.qc-appsteps__stores { display: flex; align-items: center; gap: .75rem; }
.qc-appsteps__stores > .qc-app-icon { flex: none; }
.qc-appsteps__badges { display: flex; flex-wrap: wrap; gap: .5rem; min-width: 0; }
.qc-appsteps__scan { display: flex; flex-wrap: wrap; align-items: center; gap: .875rem; }
.qc-appsteps__scan > .text-secondary { flex: 1 1 10rem; }
/* The QR that also signs the app in: a frame of the code's size from the
   start, the ring loader over it until the code arrives, and once it has run
   out or been used the scrim over the code with one button in the middle. */
.qc-qr--pairing { position: relative; }
.qc-qr__frame { display: block; width: 200px; height: 200px; }
.qc-qr__veil { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .5rem; padding: 1rem; border-radius: inherit; text-align: center; }
.qc-qr__veil[hidden] { display: none; }
.qc-qr.is-dim .qc-qr__veil { background: var(--qc-scrim); }
.qc-qr__note { font-size: var(--qc-fs-sm); line-height: 1.3; color: var(--qc-text-primary); background: var(--qc-surface-overlay); border-radius: var(--qc-radius-sm); padding: .25rem .5rem; }
.qc-appsteps__scantext { display: flex; flex-direction: column; gap: .375rem; flex: 1 1 10rem; }
.qc-appsteps__manual { padding-top: .75rem; border-top: 1px solid var(--qc-border); }
.qc-appsteps__manual summary { cursor: pointer; font-size: var(--qc-fs-sm); font-weight: 500; color: var(--qc-text-secondary); }
.qc-appsteps__manual summary:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; border-radius: var(--qc-radius-sm); }
.qc-appsteps__manual[open] summary { margin-bottom: .625rem; }

/* ==================================================================
 * §5.6 — Job chips: one job, said the same way everywhere
 *
 * A job turns up as a small thing on a grid in four places — the month's
 * tiles, the Plancia's cells, the Giornata's rows, and whatever comes next —
 * and each of them had grown its own recipe: `.qc-bchip` at a 22% tint,
 * `.qc-hpill` at 26%, `.qc-worklist__state` at 24%, the calendar's own at 22%,
 * three of them below twelve pixels. Four spellings of one idea, drifting.
 *
 * Two recipes, then, and both take their colour from `--st` — a status token
 * name set on the element, never a hex.
 *
 *   .qc-stpill   the status itself: a wash, its icon, its word
 *   .qc-jobchip  a job in a tight cell: a rail, a faint wash, a name
 *
 * The calendar's tiles are not these classes — FullCalendar owns that markup
 * and the rules live in fullcalendar.css — but they are the same three numbers:
 * one wash (`--qc-tile-wash`), a 3px rail, and nothing under 12px.
 * ================================================================== */
/* The ink drawn from a status hue: the icon of a pill, the ring of an outlined
   one, the mark on a job's tile. It keeps the hue and takes the theme's ink
   lightness (--qc-ink-l), so it holds ≥7:1 on the wash it sits on whichever
   hue it was given — the mix toward the text colour it replaces fell to 2.2:1
   on yellow in light. `min(c, .11)` leaves a neutral neutral: a grey pill must
   not turn green. Browsers without relative colours keep the old mix. */
.qc-stpill,
.qc-jobchip,
.qc-planchip,
.qc-machine__glyph,
.avatar[class*="bg-"].text-white {
  --st-ink: color-mix(in srgb, var(--st, var(--qc-primary)) 74%, var(--qc-text-primary));
}
@supports (color: oklch(from red l c h)) {
  .qc-stpill,
  .qc-jobchip,
  .qc-planchip,
  .qc-machine__glyph,
  .avatar[class*="bg-"].text-white {
    --st-ink: oklch(from var(--st, var(--qc-primary)) var(--qc-ink-l) min(c, .11) h);
  }
}
.qc-stpill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  align-self: start;
  padding: 2px 9px 2px 7px;
  border-radius: var(--qc-radius-pill);
  background: color-mix(in srgb, var(--st, var(--qc-primary)) var(--qc-status-wash), transparent);
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  line-height: 1.35;
  white-space: nowrap;
  color: var(--qc-text-primary);
}
/* The icon is mixed toward the page's own ink, because a status token is a
   pastel on white and a pale wash on near-black: neither reads on its own
   ground. Same treatment as the calendar's mark. */
.qc-stpill svg,
.qc-stpill .qc-i {
  width: 13px;
  height: 13px;
  flex: none;
  color: var(--st-ink);
}
/* Where the pill took a badge's place it sits where the badge sat. The base
   rule pins a pill to the start of its flex container, which is right for the
   lists it was written for and wrong for a cell or a header that centres what
   it holds: `--flow` hands the alignment back to the container, so a centred
   column stays centred and a row's baseline stays where it was. Every pill
   drawn by the macros, the grid and the script carries it. */
.qc-stpill--flow { align-self: auto; }

/* A job's status: the pill filled with the status colour itself, as the lists
   always wore it — the wash read too faint for the one word on a row that
   says where the job is. Dark ink on every status colour, in both themes: each
   theme has its own ramp, and both are light grounds (≥7.9:1). Same value as
   `.badge.bg-yellow` above. */
.qc-stpill--solid {
  background: var(--st, var(--qc-primary));
  color: rgba(0, 0, 0, .87);
  /* A size up from the other pills: it is the row's headline state. Still
     inside a table row at both densities (23.5px in a 28px compact box). */
  font-size: var(--qc-fs-sm);
  padding: 3px 11px 3px 9px;
  gap: 5px;
}
.qc-stpill--solid svg,
.qc-stpill--solid .qc-i,
.qc-stpill--solid .ti { color: inherit; }
.qc-stpill--solid svg,
.qc-stpill--solid .qc-i { width: 15px; height: 15px; }
.qc-stpill--solid .ti { font-size: 15px; }

/* A verdict rather than a state: the same pill, drawn as its outline. Filled
   says what a thing is ("Lavori contabilizzati"); outlined says how it stands
   against its window ("Fatto", "In ritardo di 3 giorni") — two green pills side
   by side used to be told apart by nothing. The line is the hue pulled toward
   the ink, because yellow and lime have no edge of their own on a light card. */
.qc-stpill--line {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--st-ink) 62%, transparent);
}
/* Capped to whatever holds it (a grid cell, a narrow tile): the words end in an
   ellipsis and the icon stays. */
.qc-stpill { max-width: 100%; min-width: 0; }
.qc-stpill__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The same mark where the icon is a glyph of the icon font. */
.qc-stpill:not(.qc-stpill--solid) .ti {
  font-size: 13px;
  flex: none;
  color: var(--st-ink);
}
/* Several reasons on one row ("1 in ritardo", "1 da pianificare"): side by
   side on one line, because a table row has room for one row of pills. */
.qc-stpills {
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: .3rem;
  max-width: 100%;
}
.qc-stpills > .qc-stpill { align-self: center; }

.qc-jobchip {
  display: block;
  padding: 3px 7px;
  border-radius: 7px;
  border-left: 3px solid var(--st, var(--qc-primary));
  background: color-mix(in srgb, var(--st, var(--qc-primary)) var(--qc-tile-wash), var(--qc-card-bg));
  font-size: var(--qc-fs-xs);
  font-weight: 500;
  line-height: 1.25;
  color: var(--qc-text-primary);
  overflow: hidden;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
.qc-jobchip:hover { background: color-mix(in srgb, var(--st, var(--qc-primary)) var(--qc-tile-wash-hover), var(--qc-card-bg)); color: var(--qc-text-primary); }
/* The status mark leads the name, at the size the text is set in. */
.qc-jobchip svg.qc-jobchip__mark {
  width: 12px;
  height: 12px;
  vertical-align: -2px;
  margin-right: 3px;
  color: var(--st-ink);
}
/* A job nobody is assigned to: outlined rather than filled, so the row of them
   reads as a pile waiting to be handed out.

   The rail keeps the status's colour. It used to be painted orange too — the
   colour of "to do" — which meant a scheduled job sitting in the unassigned row
   was drawn as if it were in a different status. Two facts, two channels: the
   dashed outline says nobody has it, the rail says where it is. */
.qc-jobchip--ghost {
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--qc-text-tertiary) 60%, transparent);
  border-left: 3px solid var(--st, var(--qc-primary));
  color: var(--qc-text-secondary);
}
.qc-jobchip--ghost:hover { background: color-mix(in srgb, var(--st, var(--qc-primary)) 10%, transparent); }
/* The rain mark rides the chip whose site and hours are under it, never the day. */
.qc-jobchip .qc-i {
  width: 12px;
  height: 12px;
  font-size: 12px;
  vertical-align: -1px;
  margin-right: 3px;
  color: var(--qc-info-text);
  stroke-width: 2.4;
}

/* ─────────────────────────────────────────────────────────────────────────
   §5.10  The day round — a crew's day as a list of stops, with the map beside.

   Two panels side by side on a wide screen and stacked on a narrow one. The
   list is the source of truth for both: the map is drawn from it, so dragging a
   stop redraws the line without a request.
   ───────────────────────────────────────────────────────────────────────── */
.qc-route {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}
@media (max-width: 62rem) {
  .qc-route { grid-template-columns: minmax(0, 1fr); }
}
.qc-route__panel { min-width: 0; }

/* The rounds, stacked. One where a crew is chosen, one per crew on the
   panorama — and the map beside them stays put while they are scrolled, because
   it is drawing every one of them and scrolling past it would leave the reader
   with four lists and no geography. */
.qc-route__rounds {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}
@media (min-width: 62.001rem) {
  .qc-route__mapcard { position: sticky; top: 1rem; }
}

/* The band above a round's stops carries its colour, and the accent runs down
   the card's edge so two rounds are told apart before a word is read. */
.qc-route__round { border-inline-start: 3px solid var(--qc-route-tint, var(--qc-primary)); }

.qc-route__map { height: min(52vh, 560px); min-height: 18rem; }
.qc-route__map .leaflet-container {
  width: 100%;
  height: 100%;
  background: var(--qc-surface);
  font-family: var(--qc-font-body);
  border-radius: 0 0 var(--qc-radius-card) var(--qc-radius-card);
}

.qc-route__stops { margin: 0; }
.qc-route__stop {
  position: relative;
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem 1rem;
  border-bottom: 1px solid var(--qc-divider);
}
.qc-route__stops > .qc-route__stop:last-child { border-bottom: 0; }
.qc-route__stop.is-busy { opacity: .6; }

/* The number is the row's position in the list, counted by the list.

   It used to be printed into each row by Twig, which is right exactly until
   somebody drags a stop: the badge travels with its card, so the third stop
   moved to the top still reads "3" until the page is loaded again — and this
   page exists to say which stop comes first. A counter is not a copy of the
   order, it *is* the order, so it renumbers on a drag, on ↑/↓, on Sortable's
   live insertion mid-drag, and on the row a failed save puts back. No listener
   to keep in step with the map's. */
.qc-route__stops { counter-reset: qc-route-stop; }
.qc-route__stops > .qc-route__stop { counter-increment: qc-route-stop; }
.qc-route__stops > .qc-route__stop > .qc-route__ord::before { content: counter(qc-route-stop); }

/* The number is the order, and it is the one figure on the row: tabular so a
   nine and a ten sit under each other. */
.qc-route__ord {
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--qc-primary);
  color: var(--qc-on-primary);
  font-weight: 700;
  font-size: var(--qc-fs-sm);
}
.qc-route__stop--ghost .qc-route__ord {
  background: transparent;
  color: var(--qc-text-secondary);
  border: 1px dashed var(--qc-border-strong, var(--qc-border));
}

/* The name and the address take what is left, but never less than this: the
   side of the row grows a badge at a time (status, urgency, clock, length) and
   an unbounded flex let four of them squeeze a customer down to "Condo…". */
.qc-route__who {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 12rem;
}
.qc-route__name {
  font-weight: 500;
  color: var(--qc-text-primary);
  text-decoration: none;
  /* One line, like the address under it. A four-line customer name breaks the
     rhythm the list is read by, and the whole name is one click away. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.qc-route__name:hover { text-decoration: underline; }
.qc-route__addr {
  font-size: var(--qc-grid-cell-small-size);
  color: var(--qc-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.qc-route__side {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* One line: a rain pill pushed the minutes onto a second row of their own. */
  flex-wrap: nowrap;
  white-space: nowrap;
  gap: .4rem;
  flex: 0 1 auto;
}

/* Outlined, small, and it carries its word to a screen reader — the row-action
   recipe, met by the one control on the row that is not a link. */
.qc-route__grip { cursor: grab; }
.qc-route__grip:active { cursor: grabbing; }

/* An appointment: it has an hour, so the round does not order it. It reads as a
   fact rather than as a card — no grip, a lock beside the clock, and a rail
   down the side in the primary so the eye can find the skeleton of the day
   without reading a single time. */
.qc-route__stop.is-anchored {
  border-inline-start: 3px solid var(--qc-primary);
  background: var(--qc-layer-hover);
}
.qc-route__clock {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  font-size: var(--qc-fs-sm);
  font-weight: 600;
  color: var(--qc-primary);
}
.qc-route__free {
  font-size: var(--qc-grid-cell-small-size);
  color: var(--qc-text-tertiary, var(--qc-text-secondary));
}
.qc-route__mins { font-size: var(--qc-fs-xs); color: var(--qc-text-secondary); }

/* The space between two stops, as a rule across the list.

   Two things can be true in it and they are drawn on one line, because they are
   about the same thing — what happens between arriving here and having been
   there: the drive that gets you to this stop, and, where this stop is an
   appointment, how much of the day is left before it. They belong to the gap
   rather than to either row, so neither is a badge inside one. */
.qc-route__gap {
  position: absolute;
  inset-inline: 1rem;
  top: -.6rem;
  display: flex;
  align-items: center;
  gap: .75rem;
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--qc-text-secondary);
}
.qc-route__gap::after {
  content: "";
  flex: 1;
  height: 1px;
  background: repeating-linear-gradient(
    to right,
    var(--qc-divider) 0 4px,
    transparent 4px 8px
  );
}
/* The rule is in the document above every row, the first included, so that a
   stop dragged down a place has somewhere to print the drive that now gets you
   to it. It is drawn only where something is written on it — `has-gap` — or a
   list of five would carry five dashed lines and one of them across its top. */
.qc-route__stop:not(.has-gap) > .qc-route__gap { display: none; }
.qc-route__travel { display: inline-flex; align-items: center; gap: .3rem; }
/* The slot is in the document whether or not there is a drive to print, so that
   the browser has somewhere to write one after a drag. Hidden means hidden: the
   inline-flex above would otherwise beat the UA rule for [hidden]. */
.qc-route__travel[hidden] { display: none; }
.qc-route__room { display: inline-flex; align-items: center; gap: .3rem; }
.qc-route__room.is-clash { color: var(--qc-status-orange); }

/* The room above the row is opened only where there is something on the rule.
   `has-gap` is written by the server and rewritten by the map controller when
   the drives change, so a round whose engine said nothing closes back up rather
   than leaving a band of white above every stop. */
.qc-route__stop.is-anchored,
.qc-route__stop.has-gap { margin-top: .9rem; }
.qc-route__stops > .qc-route__stop:first-child { margin-top: 0; }

/* The driving total beside the heading: its own figure, never folded into the
   one next to it. */
.qc-route__drive { display: inline-flex; align-items: center; gap: .3rem; }
.qc-route__drive[hidden] { display: none; }

.qc-route__offered { display: flex; flex-direction: column; gap: .25rem; }
.qc-route__offeredtitle {
  margin: 0 0 .25rem;
  font-size: var(--qc-grid-header-font-size);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--qc-text-secondary);
}
.qc-route__offered .qc-route__stop { padding-inline: 0; }

/* One colour per round, and it is the crew's.

   `OperatorGroup::getColor()` gives a token *name* off the crew's id — the same
   name the plan board and the calendar paint with — so the dot in the picker,
   the dot above the stops, the line on the map and the pins on it are one
   decision made in PHP and spent here. A crewless round takes the default; it is
   deliberately outside `WorkStatus::COLORS` so it can never collide with a real
   crew's.

   One class sets both halves — the tint and the ink that stays legible on it —
   and every element that carries a round's colour wears the same class. Nothing
   below reaches for a specific colour by name. */
.qc-routetint--yellow { --qc-route-tint: var(--qc-status-yellow); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--orange { --qc-route-tint: var(--qc-status-orange); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--purple { --qc-route-tint: var(--qc-status-purple); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--red { --qc-route-tint: var(--qc-status-red); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--lime { --qc-route-tint: var(--qc-status-lime); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--cyan { --qc-route-tint: var(--qc-status-cyan); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--green { --qc-route-tint: var(--qc-status-green); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--teal { --qc-route-tint: var(--qc-status-teal); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--pink { --qc-route-tint: var(--qc-status-pink); --qc-route-ink: rgba(0, 0, 0, .87); }
.qc-routetint--primary { --qc-route-tint: var(--qc-primary); --qc-route-ink: var(--qc-on-primary); }
.qc-routetint--secondary { --qc-route-tint: var(--qc-text-secondary); --qc-route-ink: var(--qc-on-primary); }

/* The mark that says which round something belongs to, wherever a round is
   named: the picker's button, its menu, the band above the stops, the key over
   the map. A shape and not a wash, so it never competes with the status pills
   on the rows beside it. */
.qc-routedot {
  flex: none;
  display: inline-block;
  width: .6rem;
  height: .6rem;
  border-radius: 50%;
  background: var(--qc-route-tint, var(--qc-primary));
}

/* The pins and the line. Leaflet writes `color:` onto the SVG stroke
   presentation attribute, where a var() never resolves — so the line takes a
   class and the stylesheet owns the token. */
/* Dashed: the order of the visits, drawn where the engine could not say how the
   road goes. Solid: a road somebody can actually drive. The difference is the
   whole of what the map is claiming, so it is never decoration. */
.qc-routeline { stroke: var(--qc-route-tint, var(--qc-primary)); }
.qc-routeroad { stroke: var(--qc-route-tint, var(--qc-primary)); }
.qc-routepin__mark {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--qc-route-tint, var(--qc-primary));
  color: var(--qc-route-ink, var(--qc-on-primary));
  font: 700 11px/1 var(--qc-font-body);
  box-shadow: 0 1px 3px rgb(0 0 0 / .3);
}
.qc-routepop { font-size: var(--qc-fs-md); line-height: 1.5; }

/* The key over the map, drawn only where there is more than one round: a legend
   of one entry is a label pretending to be a key. Each entry is a link to that
   round alone — reading one crew's day is one click from seeing them all. */
.qc-routekey {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem .75rem;
  margin-top: .5rem;
}
.qc-routekey__item {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: var(--qc-grid-cell-small-size);
  color: var(--qc-text-primary);
  text-decoration: none;
}
.qc-routekey__item:hover { text-decoration: underline; }

/* Whose round is being read, named on the control that changes it. A menu of
   links rather than a select: a select cannot carry a colour beside a name, and
   what it chooses cannot be opened in a tab or sent to somebody. */
.qc-whopick { display: inline-flex; align-items: center; gap: .4rem; }
.qc-whopick__count {
  padding: 0 .35rem;
  border-radius: 999px;
  background: var(--qc-surface-light, var(--qc-surface));
  color: var(--qc-text-secondary);
  font-size: var(--qc-fs-xs);
}
.qc-whomenu { min-width: 15rem; max-height: 70vh; overflow-y: auto; }
.qc-whomenu .dropdown-item { display: flex; align-items: center; gap: .5rem; }
.qc-whomenu__name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.qc-whomenu__count { flex: none; color: var(--qc-text-secondary); font-size: var(--qc-fs-xs); }
.qc-whomenu .dropdown-item.active .qc-whomenu__count { color: inherit; }

/* ─────────────────────────────────────────────────────────────────────────
   §5.11  Direct costs — six columns, four of them figures.

   The one hand-written list left on a fleet page (`/fleet/costs`): a job, its
   customer, and four numbers. Left to the browser, the customer column is the
   only one with nothing to bound it, so on a wide screen it swallowed every
   spare pixel — a name ended a third of the way across the line and the first
   figure sat six hundred pixels further on, with a band of empty card between
   them that the eye had to cross once per row.

   The share is declared instead, and the slack goes where the reading actually
   happens: to the columns of figures, which are right-aligned and therefore
   read as a stack. A colgroup is only binding under `table-layout: fixed` —
   the season pivot records the same lesson — and the min-width is what keeps
   the percentages from crushing the figures on a phone: the wrapper scrolls
   (`.table-responsive`), which is the honest answer for a table this wide.
   ───────────────────────────────────────────────────────────────────────── */
.qc-costs-table {
  table-layout: fixed;
  min-width: 52rem;
}
.qc-costs-table__col--job      { width: 12%; }
.qc-costs-table__col--customer { width: 28%; }
.qc-costs-table__col--hours    { width: 12%; }
.qc-costs-table__col--money    { width: 16%; }
/* The job's cell is an id over the day it was worked, and the day is one word:
   at the min-width, on a phone, "25/08/2026" was breaking across two lines
   inside a column narrow enough to make that look like two dates. */
.qc-costs-table td:first-child { white-space: nowrap; }

/* ==================================================================
   Provenance — who created a record and who last changed it
   ------------------------------------------------------------------
   components/_provenance.html.twig, for `audit.view`. A caption at the
   foot of the record: a hairline, small secondary text, no surface of
   its own. It wraps a sentence per line on a phone rather than
   scrolling, and the names are the only thing given any weight.
   ================================================================== */
.qc-provenance {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 1rem;
  row-gap: .25rem;
  margin-top: 1.5rem;
  padding-top: .75rem;
  border-top: 1px solid var(--qc-border);
  color: var(--qc-text-secondary);
  font-size: var(--qc-fs-sm);
  line-height: 1.4;
}
.qc-provenance__icon { align-self: center; font-size: 1rem; }
.qc-provenance__item + .qc-provenance__item::before {
  content: "·";
  margin-right: 1rem;
}
.qc-provenance__who { font-weight: 500; color: var(--qc-text-primary); }
.qc-provenance time[title] { cursor: help; text-decoration: underline dotted; text-underline-offset: .2em; text-decoration-color: var(--qc-border); }
@media (max-width: 575.98px) {
  .qc-provenance { flex-direction: column; align-items: flex-start; }
  .qc-provenance__icon { display: none; }
  .qc-provenance__item + .qc-provenance__item::before { content: none; }
}

/* ---- Informazioni del Cliente (report/_customer_info.html.twig) ------------
   The job sheet's tiles (.qc-sblk on a .qc-sheet__board, work-sheet.css) under
   a card header. The board's own top rule would double the header's. */
.qc-custinfo .qc-sheet__board {
  border-top: 0;
  border-radius: 0 0 var(--qc-radius-card) var(--qc-radius-card);
}
/* Three tiles across, fixed: the board's own `auto-fit` keeps a track for
   every 15rem of width as soon as one tile spans them all (the notes), and the
   three would sit in a corner of an empty row. One column below a desk. */
.qc-custinfo .qc-sheet__board { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 991.98px) {
  .qc-custinfo .qc-sheet__board { grid-template-columns: minmax(0, 1fr); }
}

/* A label over nothing but its value, two columns inside a tile: the label
   quiet, the value in the primary ink, the pair on one baseline. */
.qc-kv {
  display: grid;
  grid-template-columns: minmax(7rem, max-content) minmax(0, 1fr);
  column-gap: 1rem;
  row-gap: .45rem;
  margin: 0;
  font-size: var(--qc-fs-md);
}
.qc-kv dt {
  font-weight: 400;
  color: var(--qc-text-secondary);
}
.qc-kv dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
  color: var(--qc-text-primary);
}

/* The customer's places: the name, and under it the address when the name is
   a label rather than the address itself. */
.qc-sitelist {
  display: flex;
  flex-direction: column;
  gap: .6rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--qc-fs-md);
}
.qc-sitelist li { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.qc-sitelist__name { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; color: var(--qc-text-primary); font-weight: 500; }
.qc-sitelist__line { color: var(--qc-text-secondary); font-size: var(--qc-fs-sm); }

.qc-custinfo__note { margin: 0; white-space: pre-line; font-size: var(--qc-fs-md); color: var(--qc-text-primary); }
/* The rail's quiet figure (components/_side_nav, `count`): how many things are
   behind an entry. A fact, not a call — that is `.qc-sidenav__alert`. */
.qc-sidenav__count {
  margin-left: auto;
  min-width: 1.375rem;
  padding: 0 .4rem;
  border-radius: var(--qc-radius-pill);
  border: 1px solid var(--qc-border);
  color: var(--qc-text-secondary);
  font-size: var(--qc-fs-xs);
  line-height: 1.25rem;
  text-align: center;
}

/* ---- A document read over its list (estimate/, invoice/ _preview_modal) ----
   The estimates' register and the invoices' ledger open the same panel. The
   document is paper and its own (.qc-doc-preview, .qc-invdoc); this is only
   what holds it: a heading with the two pills, the white sheet, and a
   foot with the walk on the left and the ways on to the
   right. It hands nothing over, so there is one filled button and it leads to
   the editor's last step.
   ------------------------------------------------------------------------- */
.qc-docpreview__head { align-items: flex-start; gap: .75rem; }
.qc-docpreview__heading { display: flex; flex-direction: column; gap: .4rem; min-width: 0; }
.qc-docpreview__heading .modal-title { font-family: var(--qc-font-display); text-wrap: balance; }
.qc-docpreview__meta { display: flex; flex-wrap: wrap; gap: .4rem; }
/* White, by name: the one place the page is a sheet of paper, like the QR frame
   above. The element carries data-bs-theme="light", so the ink is the light
   theme's whichever theme the page is in. */
.qc-docpreview__body { background: #fff; color: var(--qc-text-primary); }
.qc-docpreview__warning {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  margin-bottom: .75rem;
  padding: .5rem .75rem;
  font-size: var(--qc-fs-sm);
  color: var(--qc-text-primary);
}
.qc-docpreview__loading { display: grid; place-items: center; min-height: 16rem; }
.qc-docpreview__failed { display: grid; place-items: center; min-height: 12rem; text-align: center; }
.qc-docpreview__foot { justify-content: space-between; gap: .75rem; }
.qc-docpreview__walk { display: flex; align-items: center; gap: .5rem; }
.qc-docpreview__ways { display: flex; flex-wrap: wrap; gap: .5rem; }
/* The same sheet as a page, for whoever may read an invoice and not change it. */
.qc-docpreview__body--page { padding: 0; overflow: hidden; }

/* ---- The invoice as a document (invoice/_document) ------------------------
   Laid out in the order an electronic invoice gives it: the two parties, the
   document's facts, the lines, the VAT summary beside what is due, the
   payment. Always drawn inside an element pinned to the light tokens, so the
   ink is the paper's in either theme.
   ------------------------------------------------------------------------- */
.qc-invdoc { display: flex; flex-direction: column; gap: 1rem; padding: clamp(1rem, 3vw, 2rem); font-size: var(--qc-fs-sm); line-height: 1.45; color: var(--qc-text-primary); }
.qc-invdoc__label { display: block; margin-bottom: .2rem; font-size: var(--qc-fs-xs); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--qc-text-tertiary); }
.qc-invdoc__parties { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.qc-invdoc__party { min-width: 0; padding: .65rem .8rem; border: 1px solid var(--qc-border); border-radius: var(--qc-radius-input); }
.qc-invdoc__party b { display: block; font-family: var(--qc-font-display); font-size: var(--qc-fs-xl); }
.qc-invdoc__party span:not(.qc-invdoc__label) { display: block; color: var(--qc-text-secondary); overflow-wrap: anywhere; }
.qc-invdoc__party a { color: inherit; text-decoration: none; }
.qc-invdoc__party a:hover { color: var(--qc-primary); text-decoration: underline; }
.qc-invdoc__party a .ti { margin-left: .2rem; font-size: .9em; color: var(--qc-text-tertiary); }
.qc-invdoc__facts,
.qc-invdoc__pay { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: .5rem 1rem; }
.qc-invdoc__facts { padding: .65rem .8rem; border-radius: var(--qc-radius-input); background: var(--qc-field-bg); }
.qc-invdoc__facts b,
.qc-invdoc__pay b { display: block; font-weight: 600; overflow-wrap: anywhere; }
.qc-invdoc__pay { padding-top: .75rem; border-top: 1px solid var(--qc-border); }
.qc-invdoc__scroll { overflow-x: auto; }
.qc-invdoc__table { width: 100%; min-width: 28rem; border-collapse: collapse; }
.qc-invdoc__table th { padding: .4rem .5rem; border-bottom: 1.5px solid var(--qc-text-primary); font-size: var(--qc-fs-xs); font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--qc-text-tertiary); white-space: nowrap; }
.qc-invdoc__table td { padding: .4rem .5rem; border-top: 1px solid var(--qc-border); vertical-align: top; }
.qc-invdoc__table td p { margin: 0; }
/* The VAT summary sits in half the sheet: it takes the room it is given. */
.qc-invdoc__end .qc-invdoc__table { min-width: 0; }
.qc-invdoc__end { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 1rem; align-items: start; }
.qc-invdoc__sum { display: grid; grid-template-columns: 1fr auto; gap: .25rem 1.5rem; padding: .65rem .8rem; border: 1px solid var(--qc-border); border-radius: var(--qc-radius-input); }
.qc-invdoc__sum > span:nth-child(even) { text-align: right; }
.qc-invdoc__net { margin-top: .25rem; padding-top: .4rem; border-top: 1.5px solid var(--qc-text-primary); font-size: var(--qc-fs-lg); font-weight: 700; }
@media (max-width: 640px) {
  .qc-invdoc__parties,
  .qc-invdoc__end { grid-template-columns: minmax(0, 1fr); }
}

/* A cell's first line as a link (twoLine's primaryHrefField): it reads as the
   name it is until it is pointed at. */
.qc-cell__link { color: inherit; text-decoration: none; }
.qc-cell__link:hover,
.qc-cell__link:focus-visible { color: var(--qc-primary); text-decoration: underline; }

/* ==================================================================
   A heading on a rail: the name of the entries after it.

   The settings' rail holds sixteen sections — settings, catalogues and
   read-only reports — and a list that long needs to say which is which. A
   line of text, never an entry: no hover, no link, nothing to press.
   Where the rail lies down into a strip (below lg, and the wide shell under
   1200px) there is no room for a word between two tabs, so it goes: the strip
   is the same sections in the same order.
   ================================================================== */
.qc-sidenav__heading {
  padding: .75rem .75rem .25rem;
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--qc-text-tertiary);
}
.qc-sidenav__heading:first-child { padding-top: .25rem; }
@media (max-width: 991.98px) {
  .qc-sidenav__heading { display: none; }
}
@media (max-width: 1200px) {
  .qc-shell--wide .qc-sidenav__heading { display: none; }
}
