/* ============================================================================
   components.css — the component library. Reads tokens only.
   Order: surfaces → buttons → forms → feedback → structure.
   ============================================================================ */

/* ---- Card ------------------------------------------------------------------ */
.card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
  padding: var(--sp-5);
}
.card-title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); }
.card-title + * { margin-top: var(--sp-4); }
/* base.css zeroes every margin; restore rhythm between stacked card paragraphs. */
.card p + p { margin-top: var(--sp-3); }
.section-label {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
}

/* Section head (title + trailing action above a card/table-card) — 04 §1.6 */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
  /* A title and its action are a row until they cannot be: at 320px the accountant's
     instructions page put a 195px "Natisni / shrani kot PDF" beside a two-line title
     and pushed the whole document 44px past the viewport (the 320 pass of
     screen_sweep). Wrapping costs nothing at any width where the row fits. */
  flex-wrap: wrap;
}
.section-head .card-title { margin: 0; }
/* A section's title and its explanatory note ("Kartica, keš in druga plačila — brez
   UPN QR.", 06 §7.4) travel together on the title's baseline and wrap as a unit on a
   phone; only a real ACTION is pushed to the far edge by justify-content above.
   Without the wrapper the note was flung 1100px away from the words it explains. */
.section-head-main {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  min-width: 0;
}
.section-head .hint { margin-top: 0; }

/* ---- Vertical rhythm between stacked page sections (06 §7.1) --------------------
   Three steps, and only three: 12px from a section head to the card it introduces,
   16px between two peer cards, 32px before a head that opens a NEW section. Without
   the last one a table's bottom edge touches the next section's title and two groups
   read as one (QA round 3, /za-placilo). Declared here rather than on a page sheet:
   every list screen in the app stacks exactly this way. */
.table-card + .section-head,
.card + .section-head,
.form-section + .section-head,
.callout + .section-head { margin-top: var(--sp-6); }
.table-card + .table-card,
.table-card + .card,
.card + .table-card { margin-top: var(--sp-4); }

/* ---- Buttons ---------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  height: var(--ctl-h);
  padding: 0 var(--sp-4);
  font: 500 var(--text-base)/1 var(--font-ui);
  color: var(--on-accent);
  background: var(--accent);
  border: 1px solid transparent;
  border-radius: var(--r-ctl);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background-color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}
.btn:hover { background: var(--accent-hover); text-decoration: none; }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }

.btn--secondary { background: var(--surface); color: var(--ink); border-color: var(--control-border); }
.btn--secondary:hover { background: var(--sunken); }

.btn--ghost { background: transparent; color: var(--body); }
.btn--ghost:hover { background: var(--sunken); color: var(--ink); }

.btn--danger { background: var(--danger); }
.btn--danger:hover { background: var(--danger-hover); }

.btn--sm  { height: 28px; padding: 0 var(--sp-3); font-size: var(--text-sm); }
.btn--icon { width: var(--ctl-h); padding: 0; }         /* square icon button */
.btn--icon.btn--sm { width: 28px; }
/* Login submit (04 §1.6). Height matched to the inputs above it: a 350x36 button under
   two 350x38 fields is a 2px step in a three-control stack, which reads as a mistake
   rather than a decision (QA round 7, N10). */
.btn--block { width: 100%; height: var(--input-h); }
/* page_action() wraps every topbar action label in this span so the mobile topbar can
   drop to an icon-only button (see the <900px block) without losing its name. */
.btn .btn-label { display: inline; }
/* [hidden] is a user-agent rule; .btn's own display wins unless we say otherwise, and a
   button that is hidden in markup but painted on screen is worse than no button. */
.btn[hidden], .chip[hidden] { display: none; }

/* Loading state: label hidden, centered spinner; button stays same size. */
.btn.is-loading { color: transparent; pointer-events: none; position: relative; }
.btn.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .4);
  border-top-color: var(--on-accent);
  animation: spin .6s linear infinite;
}
.btn--secondary.is-loading::after,
.btn--ghost.is-loading::after {
  border-color: var(--hairline-strong);
  border-top-color: var(--muted);
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- Forms -------------------------------------------------------------------- */
.form { max-width: var(--form-max); }
/* A card that exists only to hold a form is as wide as the form: a 1136px card
   around a 600px form is a card with an empty half (QA round 3). */
.card--form { max-width: calc(var(--form-max) + 2 * var(--sp-5)); }
/* For a card that owns the whole screen rather than sitting in a page of them. */
.card--center { margin-inline: auto; }
/* A read-only panel that follows a form has to share the form's measure, or the page
   visibly steps from 600px to the full content width halfway down (QA round 3). */
.measure-form { max-width: var(--form-max); }
.form + .measure-form, .measure-form + .measure-form { margin-top: var(--sp-5); }
.field { margin-bottom: var(--sp-4); }
.label {
  display: block;
  margin-bottom: var(--sp-2);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
}
.label .req { color: var(--danger); }             /* required marker */
.label .opt { color: var(--muted); font-weight: 400; }  /* " (neobvezno)" */

.input, .select {
  width: 100%;
  height: var(--input-h);
  padding: 0 var(--sp-3);
  font: 400 var(--text-base)/1.5 var(--font-ui);
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--control-border);
  border-radius: var(--r-ctl);
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
textarea.input { height: auto; min-height: 88px; padding: var(--sp-2) var(--sp-3); resize: vertical; }
.input::placeholder { color: var(--placeholder); }  /* decorative only — never the sole label */
.input:focus-visible, .select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring-soft);
}
.input:disabled, .select:disabled { background: var(--sunken); color: var(--muted); cursor: not-allowed; }
.input[readonly] { background: var(--canvas); }
.input:-webkit-autofill {
  -webkit-box-shadow: inset 0 0 0 100px var(--surface);
  -webkit-text-fill-color: var(--ink);
}

.input--error { border-color: var(--danger); }
.input--error:focus-visible { box-shadow: 0 0 0 3px rgba(185, 28, 28, .15); border-color: var(--danger); }
.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-1);
  margin-top: var(--sp-1);
  font-size: var(--text-sm);
  color: var(--danger);
}
.hint { margin-top: var(--sp-1); font-size: var(--text-sm); color: var(--muted); }
/* An icon that opens a sentence is a word in that sentence, but the base reset makes
   every <svg> a block (it has to: an icon-only button is not prose), so a hint that
   led with one printed the icon on a line of its own above the text — on /izvoz, on
   /prihodki and on /banka. Only the hints that actually start with an icon become a
   flex line; the rest stay ordinary paragraphs, where <b> and <a> must keep flowing
   with the words around them. Same shape .field-error has had since the DS landed. */
.hint:has(> .ico:first-child) {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
}
.hint > .ico:first-child { margin-top: 2px; }

/* Numeric / mono input variants */
.input--num  { text-align: right; font-variant-numeric: tabular-nums; }
.input--mono { font-family: var(--font-mono); font-feature-settings: "zero" 1; }

/* Select: native element, custom chevron (fixed #64748b — SVG data URIs cannot
   read currentColor; this is the documented single non-token color exception,
   value equals --muted). */
.select {
  appearance: none;
  padding-right: var(--sp-6);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
}

/* Checkboxes / radios: native controls + accent-color (crisp, accessible, free). */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  font-size: var(--text-base);
  color: var(--body);
  cursor: pointer;
}
.check input {
  width: 16px;
  height: 16px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  flex: none;
}
.check input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Date inputs: native <input type="date"> with the same .input skin. */
.input[type="date"] { padding-right: var(--sp-2); }

/* Field groups on wide forms — ONLY for tightly coupled numeric pairs. */
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
@media (max-width: 599px) { .field-row { grid-template-columns: 1fr; } }
/* A control should be about as wide as the value it holds. A 1-to-28 day picker drawn
   598px wide invites you to type an address into it (QA round 4). */
/* Half the form measure — what a control gets when it sits in a two-up .field-row.
   A money field on its own line was 598px on one screen and 293px on its two siblings
   (QA round 6); this is how a lone field claims the same measure as a paired one. */
.field--half .input, .field--half .select { max-width: var(--ctl-w); }
.field--sm .select, .field--sm .input { max-width: var(--ctl-w-sm); }
/* …and below the breakpoint where .field-row collapses there ARE no paired fields, so
   the cap has nothing left to match and only leaves one control in the form 14px short
   of every other. Affects /blagajna, /stroski and the Wolt preview alike. */
@media (max-width: 599px) {
  .field--half .input, .field--half .select { max-width: none; }
}

/* Form sections: a group of fields inside a card, labelled and separated by a hairline
   — NEVER a box of its own. A .form-section used to draw its own border, so the review
   screen showed six bordered boxes where the detail view of the same invoice showed one
   card with bands, and /uporabniki/{id}, /nadgradnja and the invoice detail each drew a
   bordered card inside a bordered card (QA round 4). The card is the only frame on
   screen; everything inside it is a band. */
.form-section > .section-label { display: block; margin-bottom: var(--sp-3); }
.form-section + .form-section,
form + .form-section {
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--hairline);
}

/* Low-confidence highlight (review screen, SPEC §6.7): amber outline. */
.field.is-lowconf .input, .field.is-lowconf .select {
  border-color: var(--status-pending-border);
  background: var(--status-pending-bg);
}
.field.is-lowconf .label::after {
  content: " · preveri";
  color: var(--warn-text);
  font-weight: 400;
}

/* ---- Callouts ------------------------------------------------------------------------------ */
.callout {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid;
  border-radius: var(--r-ctl);
  font-size: var(--text-base);
  margin-bottom: var(--sp-4);
}
.callout .ico { flex: none; margin-top: 2px; }
.callout-title { font-weight: 600; }
.callout p { margin: 0; }
.callout p + p { margin-top: var(--sp-1); }
/* Warning lists (validation warnings, export warnings) — bullets stay inside the
   callout's text column, never hanging off its icon.
   `ul.callout-list`, not `.callout-list`: base.css resets `ul[class]` to no bullets
   and no padding, and that selector outranks a bare class. */
ul.callout-list { margin: var(--sp-1) 0 0; padding-left: 1.1em; list-style: disc; }
ul.callout-list li + li { margin-top: 2px; }
/* .hint is a field hint: it sits under an input and needs no space below it. Used as a
   page lead — both audit screens do — it landed 0px above the next block, so the
   sentence read as the caption of the filter label beneath it (QA round 5). */
.hint + .filters, .hint + .filters-fold, .hint + .table-card, .hint + .card,
.hint + .section-head { margin-top: var(--sp-4); }

/* The upload tab shares the entry card's measure: a dropzone is a control, not a
   backdrop, and at full width it put a rejected file's name 900px from its reason. */
.upload-col { max-width: calc(var(--form-max) + 2 * var(--sp-5)); }

/* Pager: two steps and a position. A forensic log needs a way back past the newest
   page, and there is no honest way to pretend a hundred thousand rows are one screen. */
.pager { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); }
.pager-pos { font-size: var(--text-sm); color: var(--muted); }
.btn.is-disabled { opacity: .5; pointer-events: none; }

/* The failed-field links under a rejection summary: a jump list, not prose. */
.callout-fields { margin-top: var(--sp-1); font-size: var(--text-sm); }
.callout-fields a { font-weight: 500; }
.callout a { color: inherit; text-decoration: underline; font-weight: 500; }
.callout .check { margin-top: var(--sp-2); }
.callout--info    { color: var(--info-text);    background: var(--info-bg);    border-color: var(--info-border); }
.callout--warn    { color: var(--warn-text);    background: var(--warn-bg);    border-color: var(--warn-border); }
.callout--danger  { color: var(--status-overdue-text); background: var(--danger-bg); border-color: var(--danger-border); }
.callout--success { color: var(--success-text); background: var(--success-bg); border-color: var(--success-border); }
.callout ul { margin: var(--sp-1) 0 0; padding-left: var(--sp-4); }

/* ---- Empty states ------------------------------------------------------------------------------ */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--sp-7) var(--sp-5);
  color: var(--muted);
}
.empty .ico { color: var(--hairline-strong); margin-bottom: var(--sp-3); }
.empty--success .ico { color: var(--status-paid-text); }
.empty-title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); margin-bottom: var(--sp-1); }
.empty-body { font-size: var(--text-base); max-width: 380px; margin-bottom: var(--sp-4); }
.empty .btn { margin-top: 0; }
/* A machine-readable line under the sentence (the 500 page's request id). */
.empty-meta { font-family: var(--font-mono); font-size: var(--text-sm); margin-bottom: var(--sp-4); }

/* ---- Skeletons -------------------------------------------------------------------------------------- */
.skel { position: relative; overflow: hidden; background: var(--sunken); border-radius: var(--r-ctl); }
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .6), transparent);
  animation: shimmer 1.2s infinite;
}
@keyframes shimmer { to { transform: translateX(100%); } }
.skel--text { height: 12px; margin: var(--sp-2) 0; }
.skel--title { height: 20px; width: 40%; }
.skel--row { height: var(--row-h); border-radius: 0; margin: 0; }
.skel--block { height: 200px; }

/* ---- Breadcrumbs --------------------------------------------------------------------------------------- */
.crumbs { display: flex; align-items: center; gap: var(--sp-1); font-size: var(--text-sm); color: var(--muted); }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--ink); text-decoration: none; }
.crumbs .ico { color: var(--hairline-strong); }
.crumbs [aria-current="page"] { color: var(--ink); font-weight: 500; }

/* ---- Toasts -------------------------------------------------------------------------- */
.toast-stack {
  position: fixed;
  right: var(--sp-5);
  bottom: var(--sp-5);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-width: 360px;
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink);
  color: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-2);
  font-size: var(--text-base);
  animation: toast-in var(--dur-2) var(--ease);
}
.toast .ico { flex: none; margin-top: 2px; }
.toast--success .ico { color: var(--toast-success); }
.toast--error   .ico { color: var(--toast-error); }
.toast--info    .ico { color: var(--toast-info); }
.toast--warning .ico { color: var(--toast-warning); }
.toast-msg { flex: 1; }
.toast-close {
  flex: none;
  margin: -2px -6px 0 0;
  padding: 2px;
  border: 0;
  background: none;
  color: rgba(255, 255, 255, .7);
  cursor: pointer;
  border-radius: var(--r-ctl);
}
.toast-close:hover { color: var(--surface); }
.toast-close:focus-visible { outline: 2px solid var(--surface); outline-offset: 1px; }
.toast.is-leaving { opacity: 0; transition: opacity var(--dur-1) var(--ease); }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@media (max-width: 899px) {
  .toast-stack { left: var(--sp-4); right: var(--sp-4); bottom: var(--sp-4); max-width: none; }
}

/* ---- Dropdown menu (kebab, user menu) ------------------------------------------------ */
.kebab { position: relative; display: inline-block; }
.menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: var(--z-dropdown);
  min-width: 180px;
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-2);
  display: none;
}
.menu.is-open { display: block; }
.menu-head { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--hairline); margin-bottom: var(--sp-1); }
.menu a, .menu button {
  display: flex;
  width: 100%;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--r-ctl);
  background: none;
  font: 400 var(--text-base)/1.5 var(--font-ui);
  color: var(--body);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.menu a:hover, .menu button:hover { background: var(--sunken); color: var(--ink); text-decoration: none; }
.menu a:focus-visible, .menu button:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--accent); }
/* An action the server will always refuse says so on the item, not in a toast after a
   confirm dialog (QA round 6). Still focusable would be a lie about what it does, so it
   is a real disabled control and the reason rides in its title. */
.menu a[aria-disabled="true"], .menu button:disabled {
  color: var(--muted);
  cursor: not-allowed;
}
.menu button:disabled:hover { background: transparent; color: var(--muted); }
/* Why an item is disabled, in text rather than in a title nobody on a phone can reach. */
.menu-note {
  margin: 0;
  padding: 2px var(--sp-3) var(--sp-2);
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--muted);
}
.menu .menu-danger { color: var(--danger); }
.menu .menu-danger:hover { background: var(--danger-bg); color: var(--danger); }
.menu .ico { color: var(--muted); }
.menu .menu-danger .ico { color: inherit; }

/* ---- Table card ---------------------------------------------------------------- */
.table-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
  /* `clip` — never `hidden`: an overflow:hidden box IS a scroll container, so the
     sticky thead would resolve `top: 60px` against the CARD and sit 60px down,
     over the first row (WI-1.9 QA). `clip` still rounds the header's corners but
     creates no scrollport, so the header sticks to the viewport as intended.
     `hidden` is kept first as the fallback for engines without `clip`. */
  overflow: hidden;
  overflow: clip;
}
.tbl-scroll { overflow: visible; }          /* desktop: no scroll container, so the
                                               sticky thead sticks to the viewport */
/* border-collapse: separate is REQUIRED, not cosmetic: Chromium does not honour
   `position: sticky` on `th` inside a collapsed-border table — the header renders
   over the first row and swallows its clicks (WI-1.9 QA). Row dividers are drawn
   with per-cell border-bottom, which behaves identically with separate borders.
   position: relative makes the table the containing block for absolutely
   positioned descendants (.sr-only labels, <caption class="sr-only">); without
   it their static position leaks out of the horizontally scrolled .tbl-scroll
   and widens the DOCUMENT — a 358px phantom scroll on mobile. */
.tbl {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  position: relative;
  font-size: var(--text-table);
}

/* Sticky lives on the ROW GROUP, not on the cells: Chromium resolves a sticky
   `th`'s containing block to its row group and then applies `top` unconditionally,
   pushing the header 60px down over the first row (WI-1.9 QA measured thead y=93
   but th y=153). Chrome 91+/Firefox/Safari all support sticky `thead`. */
.tbl thead {
  position: sticky;
  top: var(--topbar-h);            /* sticks below the sticky topbar */
  z-index: var(--z-sticky);
}
.tbl th {
  height: 40px;
  padding: 0 var(--sp-4);
  background: var(--canvas);
  border-bottom: 1px solid var(--hairline);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  text-align: left;
  white-space: nowrap;
}
.tbl td {
  height: var(--row-h);
  padding: 0 var(--sp-4);
  border-bottom: 1px solid var(--hairline);
  color: var(--body);
  vertical-align: middle;
}
/* Rows scrolled to (keyboard focus, deep links) must clear the sticky topbar +
   thead, or the row lands underneath them and its actions cannot be clicked. */
.tbl tbody tr { scroll-margin-top: calc(var(--topbar-h) + 40px); }
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl tbody tr:hover td { background: var(--canvas); }
tr[data-href] { cursor: pointer; }

.tbl .num { text-align: right; font-variant-numeric: tabular-nums; }
.tbl th.num { text-align: right; }
.tbl td.primary { color: var(--ink); font-weight: 500; }
/* Data-cell links inherit their cell's ink so a column reads as text, not as a wall
   of indigo (DS §1 colour restraint); the row is clickable anyway and the anchor is
   there for keyboard users. Underline on hover keeps the affordance. Scoped to data
   cells on purpose — .cell-actions holds the kebab menu, whose own colours (and the
   red .menu-danger) must survive inside a struck-through void row. */
.tbl td.primary a, .tbl td.mono a { color: inherit; text-decoration: none; }
.tbl td.primary a:hover, .tbl td.mono a:hover { text-decoration: underline; }
.tbl td.primary a.muted { color: var(--muted); }
/* Identifiers, dates and amounts never break mid-token ("PR-2026-" / "0011",
   "14. 8." / "2026") at ANY width — the name columns are the elastic ones. */
.tbl td.mono, .tbl td.num, .tbl th.num { white-space: nowrap; }
/* A sequence column is scaffolding, not data: without this it takes an equal share of
   the width and squeezes the columns the reader actually came for (QA round 3). */
.tbl .col-seq { width: 1%; white-space: nowrap; }
.tbl td.amount  { color: var(--ink); font-weight: 600; }   /* gross totals */
.tbl .cell-actions { width: 44px; text-align: right; }

/* Long free text in a cell (email subjects, log details): one line, ellipsis, and the
   full string in the title attribute. */
.tbl td.cell-truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Overdue rows (Za plačilo; SPEC §6.8 "overdue rows red"). The tint deliberately
   survives hover — a hover that "cures" overdue reads as a glitch. The Zapadlo pill
   carries the label; the colour is never the only signal. */
.tbl tr.is-overdue td { background: var(--status-overdue-bg); }
.tbl tbody tr.is-overdue:hover td { background: var(--status-overdue-bg); }

/* Void invoices: struck through, muted, pill untouched (SPEC §6.14). */
.tbl tr.is-void td { color: var(--muted); text-decoration: line-through; }
.tbl tr.is-void td .pill { text-decoration: none; }

/* Sortable headers (server-side sort via ?sort=&dir=) */
.tbl th a.sort {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  color: inherit;
  text-decoration: none;
}
.tbl th a.sort:hover { color: var(--ink); }
.tbl th[aria-sort] a.sort { color: var(--ink); }

/* Bulk select */
.tbl .cell-check { width: 40px; padding-right: 0; }
.bulkbar {
  display: none;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-2) var(--sp-4);
  background: var(--accent-tint);
  border-bottom: 1px solid var(--hairline);
  font-size: var(--text-sm);
  color: var(--accent-strong);
}
.bulkbar.is-visible { display: flex; }
.bulkbar .btn--ghost { color: var(--accent-strong); }

/* Inner scroll containers are the only styled scrollbars in the app (DS-9). */
.tbl-scroll { scrollbar-width: thin; scrollbar-color: var(--hairline-strong) transparent; }
.tbl-scroll::-webkit-scrollbar { height: 8px; }
.tbl-scroll::-webkit-scrollbar-thumb { background: var(--hairline-strong); border-radius: var(--r-pill); }
.tbl-scroll::-webkit-scrollbar-track { background: transparent; }

/* ---- Narrow viewports: horizontal scroll with a sticky first column (<1400px) ---
   The widest table in the app (Računi, 9 columns) needs ~1130px, which only fits the
   content area from ~1400px up. Below that the table overflowed its card and was
   CLIPPED — at 1280px the last 170px, i.e. the whole actions column, was unreachable
   (WI-1.9 QA round 2). So the responsive strategy the DS defined for phones applies at
   every width where the table does not fit: scroll horizontally, keep the identifying
   first column pinned, and give up viewport-sticky headers (sticky resolves against the
   nearest scroll container, which this box now is). */
@media (max-width: 1399px) {
  .tbl-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tbl { min-width: 720px; }
  .tbl thead { position: static; }  /* vertical sticky is off inside a scroll box */
  /* The pinned column is the row's IDENTITY. Normally that is the first cell; where a
     table opens with a sequence number (.col-seq — export preview) the identity is the
     next one, and the ordinal is allowed to scroll away underneath it. */
  .tbl th:first-child:not(.col-seq), .tbl td:first-child:not(.col-seq),
  .tbl .col-seq + th, .tbl .col-seq + td {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--surface);
    border-right: 1px solid var(--hairline);
  }
  .tbl thead th:first-child:not(.col-seq),
  .tbl thead .col-seq + th { background: var(--canvas); }
  .tbl tbody tr:hover td:first-child:not(.col-seq),
  .tbl tbody tr:hover .col-seq + td { background: var(--canvas); }
}

/* ---- Mobile (<900px) ------------------------------------------------------------ */
@media (max-width: 899px) {
  /* The sticky column is the row's identity — without a floor it collapsed to
     116px and supplier names wrapped to four lines (QA round 2). */
  /* …but a checkbox column is not an identity column: .col-check was picking up the
     168px floor as a first child, which is 168px of a 390px row spent on a 24px
     control. */
  .tbl th:first-child:not(.col-seq):not(.col-check),
  .tbl td:first-child:not(.col-seq):not(.col-check),
  .tbl .col-check + th, .tbl .col-check + td,
  .tbl .col-seq + th, .tbl .col-seq + td { min-width: 168px; }
  /* Topbar actions go icon-only so the page title keeps its width; the accessible
     name lives on the button's aria-label (page_action()). */
  .topbar-actions .btn .btn-label { display: none; }
  .topbar-actions .btn { width: var(--ctl-h); padding: 0; }
}

/* ---- Phones: a table becomes a list of cards (QA round 4) ------------------------
   A 9-column ledger table in a 356px box shows two columns and hides the amount —
   and the amount is the interface in a financial tool. Below 700px each row becomes
   a card with its own labels, which app.js copies from the table's own headers, so
   no template carries a duplicate of its column names. Without JavaScript the rows
   keep the horizontal scroll above, which still works.
   Only .tbl--labelled reflows: a table app.js could not label stays a table. */
@media (max-width: 699px) {
  .tbl--labelled { min-width: 0; }
  .tbl--labelled thead { display: none; }
  .tbl--labelled, .tbl--labelled tbody, .tbl--labelled tfoot { display: block; }

  .tbl--labelled tbody tr {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 2px var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--hairline);
  }
  .tbl--labelled tbody tr:last-child { border-bottom: 0; }
  .tbl--labelled tbody tr:hover { background: transparent; }
  /* The overdue tint is painted per-cell at 1440, and the card-mode rule below clears
     the first cell's background to cancel the sticky-column treatment — which left a
     white band across the supplier name at the top of a pink card (QA round 6). On a
     card the tint belongs to the card. */
  .tbl--labelled tbody tr.is-overdue,
  .tbl--labelled tbody tr.is-overdue:hover { background: var(--status-overdue-bg); }

  /* The sticky first column is a horizontal-scroll device; there is no scroll here.
     The :not(.col-seq) has to be repeated: without it the pinned rule outweighs this
     one and every card kept a 25px hairline stub floating at its top right. */
  .tbl--labelled th:first-child:not(.col-seq), .tbl--labelled td:first-child:not(.col-seq),
  .tbl--labelled .col-seq + th, .tbl--labelled .col-seq + td {
    position: static;
    min-width: 0;
    border-right: 0;
    background: transparent;
  }
  .tbl--labelled tbody td {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    align-items: baseline;
    height: auto;
    padding: 2px 0;
    white-space: normal;
    border: 0;
  }
  .tbl--labelled tbody td::before {
    content: attr(data-label);
    font-size: var(--text-sm);
    font-weight: 400;                /* .primary/.amount embolden the CELL, not its label */
    color: var(--muted);
    white-space: nowrap;
  }
  /* The actions cell goes to the card's top-right rather than taking a line of its
     own at the bottom for a single glyph. */
  .tbl--labelled tbody tr { position: relative; }
  .tbl--labelled tbody td:not([data-label]) {
    position: absolute;
    z-index: 1;
    top: var(--sp-2);
    right: var(--sp-2);
    display: block;
    padding: 0;
  }
  .tbl--labelled tbody td:not([data-label])::before { content: none; }
  /* …so the labelled cells must keep clear of it — and the gutter has to be reserved on
     the CELLS, not on the row. Reserving it as row padding moved the row's content box
     52px left, and an absolutely positioned child of a grid container is placed against
     that content box in Chromium: the kebab landed at x=269 inside the text column and
     painted over the row's own name (44px of "Wolt Enterprises Oy"), while the gutter it
     was supposed to sit in stayed empty (QA round 6, all three reviewers' screens). */
  /* 44px kebab + 8px offset + 8px of air, measured from the row's content edge. */
  .tbl--labelled tbody tr:has(td:not([data-label])) td[data-label] {
    padding-right: calc(44px + var(--sp-2) * 2);
  }
  /* A full-width row has no kebab to keep clear of — the reserved gutter pushed an empty
     state 26px off centre (QA round 5). */
  .tbl--labelled tbody tr.row-wide:has(td) { padding: 0; }
  .tbl--labelled tbody tr.row-wide td[data-label] { padding-right: 0; }

  /* A labelled button is not a kebab. "Označi kot plačano" is 145px in a slot sized for
     a 44px glyph: it overflowed the card by 39.8px, ran 23.8px past the viewport and
     clipped its own label (QA round 6). Such a cell stops being a corner ornament and
     takes its own line at the foot of the card, where a primary action belongs. */
  .tbl--labelled tbody td:not([data-label]):has(.btn:not(.btn--icon)) {
    position: static;
    display: block;
    padding-top: var(--sp-3);
  }
  .tbl--labelled tbody tr:has(td:not([data-label]) .btn:not(.btn--icon)) td[data-label] {
    padding-right: 0;
  }

  /* Subgrid makes every cell a grid container, which BLOCKIFIES its children: a pill
     stopped being an inline-flex 73px wide and became a flex box stretched across the
     whole 226px value track, so the shape that carries the status at 1440 read as a
     full-width banner at 390. Both round-5 reviewers found it independently. */
  .tbl--labelled tbody td .pill,
  .tbl--labelled tbody td .btn {
    display: inline-flex;
    justify-self: start;
    /* And pinned to the VALUE track. A cell holding text plus a pill has three grid
       items — the ::before label, the text, the pill — so the pill wrapped onto the next
       line in column 1 and sat flush with "Datum"/"Opis"/"Kategorija" as if it were
       another label (QA round 6). Column 2 is right whether the pill shares the cell
       with text or is the whole of it. */
    grid-column: 2;
  }

  /* Same reasoning, one line down: .cell-sub is the cell's SECOND value — a filename,
     a payment reference, a supplier line. Auto-placed it landed in column 1 under the
     label, where it both read as another column name and sized the label track to its
     own width: on /banka at 390px the track grew to 300px and left the value track
     11px, clipping every amount, name and pill off the right edge of the card. */
  .tbl--labelled tbody td .cell-sub { grid-column: 2; }

  /* A void row is struck through at 1440, where the pill is an atomic inline and the
     column names live in the thead. In card mode neither holds: the decoration
     propagates into the pill (which a descendant cannot cancel) and into the generated
     ::before, so the row struck out its own column NAMES and the "Razveljavljeno" pill
     the rule exists to protect. Both reviewers found this one too. On a card the muted
     ink and that pill say "razveljavljeno" on their own. */
  .tbl--labelled tbody tr.is-void td { text-decoration: none; }
  /* …but the row's identity still carries the strike, so a void row does not read as
     two different states on two screens (QA round 8). The link is a plain inline
     descendant: the decoration lands on IT and propagates nowhere. */
  .tbl--labelled tbody tr.is-void td:first-child a { text-decoration: line-through; }

  /* The row's identity leads, in the row's own voice; the amount is the one figure
     that must never be smaller than the label beside it. */
  .tbl--labelled tbody td.primary { font-weight: 600; font-size: var(--text-lg); }
  .tbl--labelled tbody td.amount { font-weight: 600; }
  .tbl--labelled tbody td.num { text-align: left; }
  /* An empty state or a sub-heading spans everything and wants no card grid at all. */
  .tbl--labelled tbody tr.row-wide { display: block; padding: 0; }
  .tbl--labelled tbody tr.row-wide td { display: block; padding: var(--sp-3) var(--sp-4); }

  /* Totals read as a summary line, not as another record. */
  .tbl--labelled tfoot tr { display: flex; flex-wrap: wrap; justify-content: space-between;
                            gap: var(--sp-2); padding: var(--sp-3) var(--sp-4);
                            border-top: 1px solid var(--hairline-strong); }
  /* Doubled class: `.tbl tfoot td` is declared further down this file at the same
     specificity, so source order was giving each cell its own border-top — and the flex
     gap between them broke the summary rule into two segments with 24px of nothing in
     the middle (QA round 6). */
  .tbl.tbl--labelled tfoot th, .tbl.tbl--labelled tfoot td {
    display: block; border: 0; padding: 0;
  }
  .tbl--labelled tfoot td:empty { display: none; }
  /* …and each figure says which column it is the total of. */
  .tbl.tbl--labelled tfoot td[data-label]::before {
    content: attr(data-label) ": ";
    font-weight: 400;
    color: var(--muted);
  }
}

/* ---- Month control (08 §3.1) — ONE control, on every screen that has a month ------
   The arrows are the everyday move; the select is how you jump a year. Both live in one
   cluster, because two controls doing one job 95px apart is how /prihodki ended up with
   a filter form AND a header (QA round 9, N3). */
.month-nav { display: flex; align-items: center; gap: var(--sp-1); }
/* The month, and whatever the screen wants to say about it, on one line. */
.month-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
/* The select IS the month's name: no box, no chevron of its own — the arrows beside it
   already say it changes. It still opens as a native picker. */
.select--bare.month-name {
  width: auto;
  min-width: 11ch;
  height: var(--ctl-h);
  padding: 0 var(--sp-2);
  border-color: transparent;
  background-color: transparent;
  background-image: none;
  font-weight: 600;
  color: var(--ink);
  text-align: center;
  cursor: pointer;
}
.select--bare.month-name:hover { background-color: var(--sunken); }
/* The transparent border must not swallow the focus ring: .select:focus-visible sets a
   border colour, and a bare variant that overrides it unconditionally would leave a
   keyboard user with no visible focus at all. */
.select--bare.month-name:focus-visible {
  border-color: var(--accent);
  background-color: var(--surface);
}
/* 44px in both axes on a phone: the arrows were 42 wide (QA round 9, N10). */
@media (max-width: 767px) {
  .month-arrow { min-width: 44px; }
}

/* Tabular figures without the right alignment .num also brings — for a date that is a
   row's IDENTITY and therefore belongs on the left (QA round 9, N2). */
.tnum { font-variant-numeric: tabular-nums; }

/* ---- Share bar (08 §5.4) --------------------------------------------------------
   A magnitude inside a table cell. One hue — the accent — because the bar answers
   "how big", not "which category": a per-category rainbow here made the same amber
   mean "Prevzem" two cards higher up on the same page (QA round 8).
   The bar and its number are ONE inline-flex unit so the phone's card-mode table
   (which turns every cell into a subgrid) keeps them together in the value column
   instead of auto-placing the number onto a line of its own. */
.share-cell {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-variant-numeric: tabular-nums;
}
.share-bar {
  display: inline-block;
  width: 72px;
  height: 6px;
  background: var(--sunken);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.share-bar i { display: block; height: 100%; background: var(--accent); border-radius: var(--r-pill); }
/* Below 700px the cell is a grid item in the value column; the bar would otherwise
   sit on a line above its own percentage. */
@media (max-width: 699px) {
  .tbl--labelled tbody td .share-cell { grid-column: 2; }
  .share-bar { width: 56px; }
}

/* ---- Status pills ---------------------------------------------------------------- */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px;
  border: 1px solid;
  border-radius: var(--r-pill);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 18px;
  white-space: nowrap;
}
.pill::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.pill--paid    { color: var(--status-paid-text);    background: var(--status-paid-bg);    border-color: var(--status-paid-border); }
.pill--pending { color: var(--status-pending-text); background: var(--status-pending-bg); border-color: var(--status-pending-border); }
.pill--overdue { color: var(--status-overdue-text); background: var(--status-overdue-bg); border-color: var(--status-overdue-border); }
.pill--review  { color: var(--status-review-text);  background: var(--status-review-bg);  border-color: var(--status-review-border); }
.pill--neutral { color: var(--status-neutral-text); background: var(--status-neutral-bg); border-color: var(--status-neutral-border); }

/* ---- Filter chips (saved views) --------------------------------------------------- */
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: 28px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--control-border);
  border-radius: var(--r-pill);
  background: var(--surface);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--body);
  text-decoration: none;
  transition: background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.chip:hover { background: var(--sunken); text-decoration: none; }
.chip.is-active { background: var(--accent-tint); border-color: var(--accent-tint-2); color: var(--accent-strong); }
.chip .num { font-weight: 600; }

/* ---- Stat tiles -------------------------------------------------------------------- */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}
.stat-card {
  position: relative;
  display: block;
  padding: var(--sp-4) var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--dur-1) var(--ease);
}
a.stat-card:hover { border-color: var(--hairline-strong); text-decoration: none; }
a.stat-card:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring), var(--shadow-1); }
.stat-label { display: block; margin-bottom: var(--sp-1); }   /* uses .section-label */
.stat-value {
  display: block;
  font-size: var(--text-figure);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.stat-value .cur { color: var(--muted); font-weight: 500; }   /* the € sign */
.stat-card--alert .stat-value { color: var(--status-overdue-text); }
.stat-meta { display: block; margin-top: var(--sp-1); font-size: var(--text-sm); color: var(--muted); }
/* The sparkline shares the VALUE's line rather than floating in a corner: absolutely
   positioned it crossed whichever text was nearest — the meta at the bottom, the label
   at the top (both measured, QA round 8). On the figure row the number is short and the
   curve has the rest of the width, at any tile size. */
.stat-figure {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-3);
}
/* The figure never wraps: it is the tile's headline, and in a 212px tile the flex row
   was breaking "1,9 %" into "1,9" and "%" on two lines while every tile without a
   sparkline kept its number on one (QA round 9, N1b). The curve yields instead — it
   scales down, and disappears rather than pushing the number around. */
.stat-figure .stat-value { flex: 0 0 auto; white-space: nowrap; }
.stat-spark {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  height: auto;
  color: var(--accent);
  opacity: .9;
}

/* On a phone the tiles are a summary, not the page. Stacked one per row they ate 364px
   — 67 % of the first fold — and pushed the first payable row to y=566 of an 844px
   screen (QA round 7, N9). Two per row with a tighter box gives that back; "1.229,20 €"
   still fits on one line at 175px. */
@media (max-width: 699px) {
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3); }
  .stats .stat-card { padding: var(--sp-3) var(--sp-4); }
  .stats .stat-value { font-size: var(--text-title); }
  /* A tile that is the only one in its row keeps the full width rather than sitting in
     half a row with a hole beside it. */
  .stats > .stat-card:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* ---- Dialog ---------------------------------------------------------------------------- */
.dialog {
  margin: auto;
  border: 0;
  border-radius: var(--r-card);
  padding: 0;
  width: calc(100vw - 32px);
  max-width: 420px;
  box-shadow: var(--shadow-2);
  background: var(--surface);
  color: var(--body);
}
.dialog::backdrop { background: rgba(15, 23, 42, .45); }
.dialog-inner { padding: var(--sp-5); position: relative; }
.dialog-title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); margin-bottom: var(--sp-2); }
.dialog-body  { font-size: var(--text-base); margin-bottom: var(--sp-5); }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); }
/* Anchored to .dialog-inner (position: relative, above). Not every dialog has one —
   a two-sentence confirm still closes fine with Prekliči — but a dialog whose content
   can grow past the viewport needs a way out that never scrolls out of reach. */
.dialog-close {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--r-ctl);
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.dialog-close:hover { background: var(--sunken); color: var(--ink); }
.dialog-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A dialog whose body is a search field and its own results list, not one or two short
   sentences — 420px wrapped a supplier name and its amount onto three lines, and a
   result list long enough to need scrolling showed exactly one row before this (QA
   round: owner screenshot). Height-capped and internally scrollable rather than
   letting content run past the viewport with no way to reach what fell off it; the
   actions row stays pinned to the bottom of that scroll so Poveži/Prekliči are never
   the thing that scrolled away. */
.dialog--wide {
  max-width: 560px;
  max-height: 85vh;
  overflow-y: auto;
}
.dialog--wide .dialog-actions {
  position: sticky;
  bottom: calc(-1 * var(--sp-5));
  margin: var(--sp-4) calc(-1 * var(--sp-5)) calc(-1 * var(--sp-5));
  padding: var(--sp-3) var(--sp-5) var(--sp-5);
  background: var(--surface);
  border-top: 1px solid var(--hairline);
}

/* ---- Form action rows + card stacking (02 §6.2) --------------------------------------- */
.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
/* A submit button that touches the card above it reads as part of that card. */
.form-section + .form-actions, .card + .form-actions { margin-top: var(--sp-5); }
@media (max-width: 599px) {
  /* Long Slovenian labels ("Pošlji obvestilo računovodstvu") are wider than a phone;
     .btn is nowrap by design, so on mobile the buttons stack full-width instead of
     pushing the page sideways. */
  .form-actions .btn { width: 100%; white-space: normal; }
  /* A button that carries its own POST is wrapped in a <form>, so the FORM is the flex
     item — without this the wrapped button stays narrow while its bare siblings go
     full width, which reads as two different kinds of button. */
  .form-actions > form { flex: 1 1 100%; }
}
.card + .card { margin-top: var(--sp-4); }                                /* stacked panels */

/* ---- Key/value read-only blocks (settings System panel) --------------------------------- */
.kv { display: grid; grid-template-columns: 220px 1fr; gap: var(--sp-3) var(--sp-4); margin: 0; }
.kv dt { font-size: var(--text-sm); font-weight: 500; color: var(--muted); }
.kv dd { margin: 0; color: var(--body); }
.kv dd .hint { margin-top: 2px; }
/* Stacked, a uniform 4px gap made seven label/value PAIRS read as fourteen evenly
   spaced lines. The label hugs its value; the pairs get the air (QA round 5). */
@media (max-width: 599px) {
  .kv { grid-template-columns: 1fr; gap: 0; }
  .kv dt { margin-top: var(--sp-3); }
  .kv dt:first-of-type { margin-top: 0; }
}

/* ---- Filter bar (search + submit above a list) ------------------------------------------ */
/* A search bar with a visible label: the label sits on its own line above the row, the
   field and the button share the row. align-items:end keeps the button's baseline with
   the input's when the label wraps. */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
  max-width: 600px;
}
.filter-bar .label { flex: 1 0 100%; margin-bottom: 0; }
.filter-bar .input { flex: 1; }

/* ---- Filter grid (invoice list, 05 §6.4) ------------------------------------------------ */
/* 1fr tracks made a filter <select> 371px wide on a 1440px page while the form
   <select> beside it was 294px — the same control at two widths, on one screen. The
   track is capped at the control measure and the bar starts at the left edge instead
   of stretching to the content width (QA round 4). */
.filters {
  display: grid;
  grid-template-columns: repeat(3, minmax(180px, var(--ctl-w)));
  justify-content: start;
  gap: var(--sp-3);
  align-items: end;
  margin-bottom: var(--sp-4);
}
@media (max-width: 899px) {
  .filters { grid-template-columns: repeat(2, minmax(160px, var(--ctl-w))); }
}
.filters .field { margin-bottom: 0; min-width: 0; }
/* The month picker on /izvoz sits in a wrapper with the "last export" line under it; the
   wrapper had no bottom margin, so that line was flush against the warning callout below
   it — a 0.0px seam on a page whose other gaps are 12/16/32 (QA round 6). */
.export-head { margin-bottom: var(--sp-5); }
.export-head .filters { margin-bottom: var(--sp-2); }
/* A <select> contributes its WIDEST OPTION as min-content width, so a supplier column
   grabbed 225px of a 358px phone and left "Kategorija" 121px wide — narrower than its
   own chevron plus two words (QA round 3). min-width:0 above frees the track; the
   single column below makes every control the same, readable width. */
.filters .select, .filters .input { width: 100%; }
/* With scripting on, a filter <select> applies itself on change and this button submits
   the same state a second time — it reads as the control that applies the filter and
   never is one (QA round 7, N-10). It IS the no-JS path, so it is hidden rather than
   removed, and only for the grid filter bars whose every control auto-submits; the
   supplier search keeps its button, because typing has to be submitted by something. */
html.js .filters .filters-actions { display: none; }

.filters-actions { display: flex; gap: var(--sp-2); align-items: center; }
@media (max-width: 599px) {
  .filters { grid-template-columns: 1fr; }
  .filters-actions { flex-wrap: wrap; }
  .filters-actions .btn { flex: 1 1 auto; }
}
/* Collapsed filters (mobile): <details> so it works with no JavaScript at all. */
.filters-fold { margin-bottom: var(--sp-4); }
.filters-fold > summary {
  display: none;                       /* desktop shows the bar itself, never a toggle */
}
/* A closed <details> in Chromium hides its contents through ::details-content's
   content-visibility, which display:grid on the child cannot reach — so the desktop
   filter bar stopped painting entirely while still reporting a 191px box. Desktop never
   folds, so the content box is forced open there; engines without the pseudo fall
   through to .filters{display:grid} beating the old UA rule and land in the same place. */
.filters-fold::details-content { content-visibility: visible; block-size: auto; }
@media (max-width: 767px) {
  .filters-fold > summary {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: 44px;
    padding: 0 var(--sp-4);
    font-size: var(--text-base);
    font-weight: 500;
    color: var(--ink);
    background: var(--surface);
    border: 1px solid var(--control-border);
    border-radius: var(--r-ctl);
    cursor: pointer;
    list-style: none;
  }
  .filters-fold > summary::-webkit-details-marker { display: none; }
  .filters-fold > summary .ico { color: var(--muted); }
  .filters-fold > summary .fold-count {
    margin-left: auto;
    font-size: var(--text-sm);
    color: var(--muted);
  }
  .filters-fold[open] > summary { margin-bottom: var(--sp-3); }
  .filters-fold .filters { margin-bottom: 0; }
  .filters-fold:not([open]) > .filters { display: none; }
  .filters-fold:not([open])::details-content { content-visibility: hidden; block-size: 0; }
}

/* `.tbl td { color: var(--body) }` outranks a bare `.muted` on the same cell, so
   <td class="muted"> in both log tables rendered at full weight and the de-emphasis
   never happened (QA round 5). The link case one rule below was already patched for the
   same reason. */
.tbl td.muted, .tbl th.muted { color: var(--muted); }

/* Secondary line inside a table cell (original filename under an unmatched supplier). */
.cell-sub {
  display: block;
  font-size: var(--text-sm);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}
/* …unless the cell IS the value the reader came for. A generated filename is the
   row-to-file mapping for the whole export, and truncating it to 220px with no title
   and no scroll left half of it unreachable (QA round 6). It wraps instead, on the
   separators a filename actually has. */
.cell-sub.cell-wrap {
  max-width: 320px;
  white-space: normal;
  overflow: visible;
  overflow-wrap: anywhere;
  line-height: 1.35;
}

/* Overdue due-date emphasis — always paired with the "Zapadlo" pill, never alone. */
.due-overdue { color: var(--danger); font-weight: 600; }

/* The JS-off answer stack. Same callouts the forms use, just gathered at the top of
   the content where the toast would otherwise have appeared. Only ever rendered
   inside <noscript>, so with scripting on it does not exist. */
.flash-noscript { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-4); }
.flash-noscript .callout { margin: 0; }

/* A region we made focusable so the keyboard can scroll it (report tables, the JSON
   example on /prihodki) must SHOW that focus like every other focusable thing — a
   tabindex with no ring trades one a11y defect for another. */
.table-scroll:focus-visible,
.code-block:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- Dropzone (02 §6.3) ------------------------------------------------------ */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-5);
  background: var(--surface);
  border: 1.5px dashed var(--hairline-strong);
  border-radius: var(--r-card);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}
.dropzone:hover { border-color: var(--accent); }
/* Two selectors, one state. Only /racuni/nov's zone is the focusable element itself
   (a div with tabindex); everywhere else the zone IS a <label> wrapping an sr-only
   file input, so the focus lands on a 1×1 clipped box and `.dropzone:focus-visible`
   never matched — the primary action of the revenue-import and Wolt tabs had no
   visible focus at all (QA round 10, N4). :has() carries :focus-visible outward to
   the box a keyboard user can actually see. */
.dropzone:focus-visible,
.dropzone:has(:focus-visible) { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring-soft); }
.dropzone.is-dragover { border-color: var(--accent); background: var(--accent-tint); }
.dropzone .ico { color: var(--muted); }
.dz-title { font-size: var(--text-lg); font-weight: 500; color: var(--ink); }
.dz-hint  { font-size: var(--text-sm); color: var(--muted); }

/* Compact form (05 §13.3): same zone, less air — it sits inside the review screen's
   left pane rather than owning a page. */
.dropzone--compact { padding: var(--sp-5) var(--sp-4); }
.dropzone--compact .dz-title { font-size: var(--text-base); }
.dz-chosen {
  margin-top: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.dz-list { margin-top: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); }
.dz-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-ctl);
}
.dz-item .ico { flex: none; color: var(--muted); }
/* Leading icon: all three states are server-rendered into #dz-item-tpl (upload.js
   must never build SVG — 05 §4.3) and CSS decides which one is on. file-text while the
   row is working, circle-check when it lands, circle-alert when it fails. */
.dz-ico { display: none; }
.dz-item .dz-ico--file { display: block; }
.dz-item.is-done .dz-ico--file,
.dz-item.is-error .dz-ico--file { display: none; }
.dz-item.is-done  .dz-ico--done  { display: block; }
.dz-item.is-error .dz-ico--error { display: block; }
.dz-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-base);
  color: var(--ink);
}
.dz-progress {
  width: 140px;
  height: 4px;
  flex: none;
  border-radius: var(--r-pill);
  background: var(--sunken);
  overflow: hidden;
}
.dz-progress > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width var(--dur-2) var(--ease);
}
.dz-item.is-extracting .dz-progress > i {
  width: 100% !important;
  animation: dz-indeterminate 1.2s var(--ease) infinite;
  transform-origin: left;
}
@keyframes dz-indeterminate {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
/* Progress bar only exists while the file is moving; done/error rows drop it so the
   status text sits where the eye already is (05 §4.2). */
.dz-item.is-done .dz-progress, .dz-item.is-error .dz-progress { display: none; }
.dz-status { flex: none; font-size: var(--text-sm); color: var(--muted); white-space: nowrap; }
.dz-item.is-done  .dz-status { color: var(--status-paid-text); }
.dz-item.is-done  .ico       { color: var(--status-paid-text); }
.dz-item.is-error .dz-status { color: var(--danger); }
.dz-item.is-error .ico       { color: var(--danger); }
/* Phones: the filename keeps line one, status + actions wrap to line two, and the
   progress bar goes away (the percentage is already in the status text). Without this
   a single .dz-item pushed the page 60px wider than the viewport. */
@media (max-width: 599px) {
  .dz-item { flex-wrap: wrap; }
  .dz-item .dz-progress { display: none; }
  .dz-status { flex: 1 1 100%; white-space: normal; }
}
/* Per-item action links: "Preglej →", "Uredi ročno →", "Poskusi znova", dup warning. */
.dz-action { flex: none; display: inline-flex; align-items: center; gap: var(--sp-1);
             font-size: var(--text-sm); font-weight: 500; color: var(--accent-strong);
             background: none; border: 0; padding: 0; cursor: pointer; }
.dz-action:hover { text-decoration: underline; }
/* The duplicate warning is server-rendered too (triangle + label + link), so a state
   change never has to rebuild it: JS fills the internal number and flips .has-dup. Its
   amber must survive inside an is-done row, whose .ico rule would paint it green —
   hence the icon is targeted directly. */
.dz-dup { display: none; color: var(--warn-text); white-space: nowrap; }
.dz-dup .ico { color: var(--warn-text); }
.dz-item.has-dup .dz-dup { display: inline-flex; }
@media (max-width: 599px) {
  /* The filename owns line one; the warning takes a line of its own rather than
     truncating the very name the reader is looking for. */
  .dz-item.has-dup .dz-dup { flex: 1 1 100%; white-space: normal; }
}

/* ---- Sum check (invoice amounts, 05 §12.4) ---------------------------------- */
/* Mirrors server validation V1 live while typing. It never blocks a save — it is a
   reading aid, and the server stays the authority on the numbers. */
.sum-check {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: var(--sunken);
  border-radius: var(--r-ctl);
  font-size: var(--text-sm);
  /* --body, not --muted: on the sunken ground the muted grey measured 4.23:1 at 13px,
     under AA, and this line is the screen's headline verification — it should be the
     most legible thing in the card, not the least (QA round 5). */
  color: var(--body);
  font-variant-numeric: tabular-nums;
}
.sum-check b { font-weight: 600; color: var(--ink); }
.sum-ok   { font-weight: 600; color: var(--status-paid-text); }
.sum-bad  { font-weight: 600; color: var(--danger); }
/* Neither right nor wrong yet — a verdict the form has not earned. */
.sum-wait { font-weight: 500; color: var(--muted); }

/* The payment screen is one column the width of the slip: the slip is a physical
   object with a fixed measure, and a full-width card beneath a 760px slip made the
   page read as three stacked widths (QA round 4). */
.pay-col { max-width: 760px; }
.pay-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  justify-content: flex-end;
  margin: var(--sp-3) 0 var(--sp-4);
}
@media (max-width: 599px) {
  .pay-actions { justify-content: stretch; gap: var(--sp-2); }
  .pay-actions .btn { flex: 1 1 100%; }
  .pay-actions .btn--ghost { border-color: var(--hairline); background: var(--surface); }
}

/* ---- UPN slip — THEMING-EXEMPT (frozen white/ink, DECISIONS §3.7) --------------- */
/* A payment slip is read next to a bank app and often printed; its colours are frozen
   on purpose and must survive any future dark theme untouched. */
.upn-slip {
  background: #ffffff;
  color: #0f172a;
  border: 1px solid var(--hairline);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
  padding: var(--sp-5);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-6);
  max-width: 760px;
}
.upn-qr {
  width: 300px;
  height: 300px;
  background: #ffffff;   /* THEMING-EXEMPT (DECISIONS §3.7) — the QR's quiet zone must stay white */
  flex: none;
}
.upn-qr img, .upn-qr svg {
  width: 100%;
  height: 100%;
  display: block;
  image-rendering: pixelated;    /* crisp modules if the PNG is upscaled */
}
/* Two columns, so the pair of half-width fields (Rok plačila / Koda namena) can live
   as siblings of the full-width ones instead of inside a nested wrapper — a <dl> may
   contain a div, but not a div of divs (QA round 10, N-9). */
.upn-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
              gap: var(--sp-3); align-content: start; min-width: 0; }
.upn-field { grid-column: 1 / -1; min-width: 0; }
.upn-field--half { grid-column: auto; }
.upn-field dt {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  margin-bottom: 2px;
}
.upn-field dd { margin: 0; }
.upn-value {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-feature-settings: "zero" 1;   /* slashed zero: O vs 0 in IBANs */
  font-size: 15px;
  color: #0f172a;   /* THEMING-EXEMPT (DECISIONS §3.7) */
  /* Wrap anywhere rather than break everywhere: `anywhere` breaks a long unbroken
     token (an IBAN, a reference) but still prefers the spaces in a sentence, so the
     payer line no longer splits a postcode across two lines — "123" / "6 Trzin" on a
     390px screen, on the one slip whose job is to be read against a bank app
     (QA round 9, N-3). */
  overflow-wrap: anywhere;
}
.upn-value--amount { font-size: 20px; font-weight: 500; }
.upn-slip .copy-btn { flex: none; }

/* ==== charts (Phase 2, /analitika) ==========================================
   A canvas is the decoration; the table under it carries the numbers. Without
   scripting the canvas is a blank rectangle where a chart should be, so it is
   removed from the page entirely and the table becomes the whole answer.
   print.css already does the same swap for paper (02 §9.2). */
html:not(.js) .chart-canvas { display: none; }
@media (max-width: 767px) {
  .upn-slip { grid-template-columns: 1fr; justify-items: center; }
  .upn-fields { width: 100%; }
}

/* QR placeholder when the payload cannot be built (06 §1.5). */
.upn-qr--blocked {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 300px;
  height: 300px;
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--r-ctl);
  padding: var(--sp-4);
  color: var(--muted);
  font-size: var(--text-sm);
  text-align: center;
}

/* ---- Blagajna layout: balance above the entry form (06 §9.2) -------------------
   The balance sat in a 320px column BESIDE the entry card. The card is 391px tall and
   the tile 111px, so the layout's own shape carved a 320x280 void at the page's left
   margin — negative space that reads as a missing element rather than as breathing room
   (QA round 4). Stacked, both start at the same left rail as the filter bar and the
   table below them, and the screen has one column instead of two. */
/* A negative money figure in a tile. Deliberately NOT red — 06 §9.3: a till in the
   minus is a counting error to investigate, not an alert to panic about — but it must
   not look identical to a healthy balance either, so the weight and the meta line below
   it carry the difference (QA round 7, N-8). */
.stat-value.is-negative { color: var(--body); }
.stat-value.is-negative + .stat-meta { color: var(--warn-text); font-weight: 500; }
.stat-value.is-negative + .stat-meta .ico { vertical-align: -2px; margin-right: 4px; }

/* Two tiles side by side, together as wide as the entry card below them: the balance
   and how the month moved. One 320px tile in a 648px band left 72 % of it empty while
   the month's change sat below the fold under the table (QA round 7, N-7). */
.cash-top { display: grid; grid-template-columns: repeat(2, minmax(0, 320px));
            gap: var(--sp-4); margin-bottom: var(--sp-4); }
@media (max-width: 699px) { .cash-top { grid-template-columns: minmax(0, 1fr); } }
/* Month picker sits alone above the table; it needs none of the filter grid's columns. */
.filters--month { grid-template-columns: 200px auto; }

/* Table footer totals: hairline above, same weight as an amount cell. */
.tbl tfoot th, .tbl tfoot td {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--hairline-strong);
  font-weight: 600;
  color: var(--ink);
  text-align: left;
}
.tbl tfoot td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* The summary band belongs to the whole ROW. It used to be a background on the first
   cell only — because that cell is the pinned column and carries a fill for the
   horizontal scroll — so 68 % of the footer was unshaded and the total the band exists
   to frame sat outside it, ending in a hard edge that lined up with no column (QA
   round 7, N-4). The row paints; the cells, pinned one included, keep out of it. */
.tbl tfoot tr { background: var(--canvas); }
.tbl tfoot th, .tbl tfoot td,
.tbl tfoot th:first-child:not(.col-seq), .tbl tfoot td:first-child:not(.col-seq),
.tbl tfoot .col-seq + th, .tbl tfoot .col-seq + td { background: transparent; }

/* ---- Tabs (canonical spec: plan file 05 §4.1) ------------------------------- */
.tabs { display: flex; gap: var(--sp-1); border-bottom: 1px solid var(--hairline);
        margin-bottom: var(--sp-5); }
.tab  { padding: var(--sp-2) var(--sp-3); margin-bottom: -1px; font-size: var(--text-base);
        font-weight: 500; color: var(--muted); text-decoration: none;
        border-bottom: 2px solid transparent; }
.tab:hover { color: var(--ink); text-decoration: none; }
.tab.is-active { color: var(--accent-strong); border-bottom-color: var(--accent); }

/* ---- Touch targets on phones (<768px) — DS-9 §10.3 rule 6, DS-10 row 8 ---------
   44px is a floor for the FINGER, not for the ink. Text-bearing small controls simply
   grow; the icon-only kebab becomes a 44×44 square, and because it is a GHOST button
   its surface stays invisible until pressed — the row still shows a 16px glyph, not a
   slab. Desktop keeps the 28px compact sizes: this block exists only below 768px. */
@media (max-width: 767px) {
  .btn, .btn--sm { min-height: 44px; }
  .btn--icon, .btn--icon.btn--sm, .topbar-actions .btn { width: 44px; }
  .chip { min-height: 44px; padding: 0 var(--sp-4); font-size: var(--text-base); }
  .menu a, .menu button { min-height: 44px; }
  .tab { display: inline-flex; align-items: center; min-height: 44px; }
  .dz-action { min-height: 44px; }
  /* The controls a form is actually made of. On the review screen these ARE the
     screen, and 38px is not a thumb target. */
  .input, .select { height: 44px; }
  textarea.input { min-height: 96px; }
  .check { min-height: 44px; align-items: center; }
  .check input { width: 22px; height: 22px; margin-top: 0; }
}
