/* ==========================================================================
   Sandi Ticket - design system
   --------------------------------------------------------------------------
   The original app was seven near-duplicate stylesheets (add.css, edit.css and
   import-export.css were largely the same file), which is why a fix in one page
   silently missed the others. This is one file with tokens and shared
   components.

   The centred, single-column layout is deliberately preserved: it reads calm
   and focused, and it was the one thing about the original design that worked
   really well. What changes is that the centring is now driven by two tokens
   instead of ad-hoc max-widths per page.
   ========================================================================== */

/* The brand font, declared under its own name so it can be referenced
   explicitly. One regular weight only, which is all the app uses.
   
   NOTE the `../`: this file lives in `css/`, so a bare `url('fonts/...')`
   resolves to `css/fonts/...` and 404s. That is exactly what was happening -
   the font silently never loaded and every screen fell back to a system face,
   which is invisible unless you check the network log. */
@font-face {
  font-family: 'SNPro-Regular';
  src: url('../fonts/SNPro-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

/* Kept as an alias so any reference to the short name still resolves. */
@font-face {
  font-family: 'SNPro';
  src: url('../fonts/SNPro-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

:root {
  /* --- Brand ------------------------------------------------------------ */
  --green-600: #43a047;
  --green-700: #388e3c;
  --green-800: #2e7d32;
  --green-50: #eef7ee;
  --blue-500: #2196f3;
  --blue-600: #1e88e5;
  --red-500: #f44336;
  --red-700: #b71c1c;
  --amber-500: #ff9800;
  /* A wash for the one notice that is a warning rather than a success or an
     error - the Zarada search box being given a phrase it cannot read. Amber's
     light tint has existed implicitly in the status pills; this names it. */
  --amber-50: #fff5e6;
  --slate-500: #707070;

  /* --- Surfaces --------------------------------------------------------- */
  --bg: #f4f6f8;
  --surface: #ffffff;
  --surface-2: #fdfdfd;
  /* A third surface for things that sit *on* a card and need to read as
     recessed: table row hover, and the profit track behind its bar. */
  --surface-3: #fafbfc;
  --surface-4: #e3e8ee;
  /* The knob on a toggle switch, which must stay light against its track. */
  --switch-knob: #ffffff;
  /* The dark toast/inverse surface, and the ink that sits on it. Both flip in
     dark mode, where an almost-black toast would vanish into the background. */
  --toast-bg: #1f2933;
  --toast-ink: #ffffff;
  --border: #dfe3e8;
  --border-strong: #c8ced6;
  /* The hairline between table rows. Lighter than `--border` on purpose, so the
     rows separate without the table reading as a grid. It must be a token: a
     near-white divider is invisible on a light surface and a glaring white line
     on a dark one. */
  --row-divider: #eef1f4;
  /* Hover wash on a ticked damage tile. A deeper tint of the same green. */
  --check-checked-hover: #e3f1e4;

  /* --- Scrollbar -------------------------------------------------------- */
  --scrollbar-thumb: #c2c9d2;
  --scrollbar-thumb-hover: #9aa4b0;

  /* --- Inline notices --------------------------------------------------- */
  --alert-ok-bg: #e8f5e9;
  --alert-ok-border: #c8e6c9;
  --alert-err-bg: #fdecea;
  --alert-err-border: #f5c6c3;
  --alert-info-bg: #e8f2fe;
  --alert-info-ink: #0b4f9e;
  --alert-info-border: #cfe2fb;

  /* --- Text ------------------------------------------------------------- */
  --text: #1f2933;
  --text-muted: #64748b;
  --text-faint: #94a3b8;

  /* --- Layout ----------------------------------------------------------- */
  /* Two content widths, replacing the per-page max-widths of 1200px here and
     600px there. `wide` is the dashboard, `narrow` is every form. */
  --content-wide: 1240px;
  --content-narrow: 640px;
  --gutter: 20px;

  /* --- Rhythm ----------------------------------------------------------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;

  /* --- Shape & depth ---------------------------------------------------- */
  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;
  --radius-pill: 999px;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);
  --shadow: 0 4px 12px rgba(16, 24, 40, .08);
  --shadow-lg: 0 12px 32px rgba(16, 24, 40, .14);

  /* Focus glows are switched off application-wide, by request. Because every
     focus style in this file references this one token, neutralising it here
     removes them all at once rather than in eight places. */
  --focus-ring: 0 0 0 0 transparent;
}

/* ==========================================================================
   Dark theme
   ==========================================================================
   Only the tokens that describe *how light the page is* change here. Every
   component already reads those tokens, so nothing below this block needs a
   dark variant of its own - which is the whole reason the palette lives in one
   place.

   The brand colours are kept but lifted a step: #2e7d32 is a fine green on
   white and a muddy one on a near-black surface, so the greens used as *text*
   move up the ramp while the solid fills stay put.
   ========================================================================== */

[data-theme='dark'] {
  /* --- Surfaces --------------------------------------------------------- */
  --bg: #12161c;
  --surface: #1a2029;
  --surface-2: #1e2530;
  --surface-3: #232b37;
  --surface-4: #2c3542;
  --switch-knob: #e8edf3;
  --toast-bg: #2f3a48;
  --toast-ink: #f1f5f9;
  --border: #2c3542;
  --border-strong: #3d4857;
  /* Faint, like its light counterpart - dark enough to separate rows, far from
     the white line a light-mode divider would draw here. */
  --row-divider: #232b37;
  --check-checked-hover: #24382a;

  /* --- Text ------------------------------------------------------------- */
  --text: #e8edf3;
  --text-muted: #9aa7b6;
  --text-faint: #74828f;

  /* --- Scrollbar -------------------------------------------------------- */
  --scrollbar-thumb: #3d4857;
  --scrollbar-thumb-hover: #52606f;

  /* --- Inline notices --------------------------------------------------- */
  /* Deep tints of the same hues, with lighter borders, so the notices still read
     as "green / red / blue" without a glowing pastel block on a dark page. */
  --alert-ok-bg: #1b2a1e;
  --alert-ok-border: #2f5136;
  --alert-err-bg: #2b1c1c;
  --alert-err-border: #5a3030;
  --alert-info-bg: #182634;
  --alert-info-ink: #7fb6f0;
  --alert-info-border: #2b4a6b;

  /* --- Brand, adjusted for legibility on dark --------------------------- */
  --green-800: #6ec071;
  --green-700: #4caf50;
  --green-50: #1c2a20;
  --blue-500: #4da3f5;
  --blue-600: #3b93e8;
  --red-700: #ef9a9a;
  --slate-500: #9aa7b6;
  /* The same warning wash, deepened so it does not glow on a dark page - the
     rule the inline notices above follow. */
  --amber-50: #2e2415;

  /* --- Depth ------------------------------------------------------------ */
  /* Shadows do almost nothing against a dark background, so they are deepened
     rather than removed: the separation still has to come from somewhere, and
     a slightly stronger shadow plus the lighter surface above is what gives it. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .34);
  --shadow: 0 4px 12px rgba(0, 0, 0, .42);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, .55);
}

/* --------------------------------------------------------------------------
   Reset
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  /* The app manages its own scrolling: the shell is exactly one viewport tall and
     `.scroll-host` inside it does the scrolling. Without this the document itself
     would also scroll, and the window scrollbar would run the full height of the
     window - up beside the close button. */
  height: 100%;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   Scrollbar
   --------------------------------------------------------------------------
   Chromium draws its own scrollbar with stepper arrows at each end, which reads
   as dated next to the rest of the interface. Styling `::-webkit-scrollbar`
   replaces it with a plain rounded thumb.

   Note the arrows cannot be hidden on their own - setting a style on
   `::-webkit-scrollbar` replaces the whole scrollbar, so they go with it.
   -------------------------------------------------------------------------- */

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

/* No `::-webkit-scrollbar-button` sizing needed: the arrows only exist when that
   pseudo-element is styled, and it is left alone here. */
::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: 999px;
  /* The transparent border insets the thumb, so it reads as a floating pill
     rather than a bar filling the full gutter. */
  border: 2px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
  background-clip: content-box;
}

/* The corner where the two bars meet, which Chromium otherwise fills with a
   grey square. */
::-webkit-scrollbar-corner {
  background: transparent;
}

/* The whole application uses the brand face. The two names are the same file;
   `SNPro-Regular` is listed first so the intent is explicit, and the plain
   system stacks after it are only a safety net for a glyph the brand font does
   not carry (the UI includes arrows, a caret and a euro sign in places). */
body {
  font-family: 'SNPro-Regular', 'SNPro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Form controls do not inherit the body font in every engine, so they are
   pointed at it explicitly. This is what previously left `<select>` and
   `<textarea>` rendering in the system font. */
input,
select,
textarea,
button,
optgroup,
option {
  font-family: inherit;
}

/* Anything that would otherwise fall back to a serif or monospace default. */
code,
kbd,
pre,
samp {
  font-family: 'SNPro-Regular', 'SNPro', ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* --------------------------------------------------------------------------
   Layout primitives
   -------------------------------------------------------------------------- */

.shell {
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* The scrolling half of the shell.
 *
 * Making this the scroll container - rather than letting the document scroll -
 * is what puts the window scrollbar below the titlebar instead of alongside the
 * close button. `min-height: 0` lets it shrink inside the flex column; without
 * it a flex child refuses to go below its content height and would push the
 * titlebar off instead of scrolling.
 */
.scroll-host {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

/* The centring that the original design already had. */
.centered {
  width: 100%;
  max-width: var(--content-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.centered--narrow {
  max-width: var(--content-narrow);
}

/* --------------------------------------------------------------------------
   Top bar
   -------------------------------------------------------------------------- */

/* The header is not a panel: it blends into the page background so the logo
   and the centred rows sit on the same surface as everything below, rather
   than on a white bar.
   
   It is deliberately NOT sticky. A transparent sticky header would let the
   table scroll through the logo, which looks broken; and with the actions
   moved to the footer there is nothing here worth pinning. */
/* ==========================================================================
   Custom titlebar
   ==========================================================================
   Replaces the native Windows chrome (`"decorations": false`), so this bar owns
   three jobs: it identifies the window, it is the drag handle, and it holds the
   window controls.

   Styled as part of the app rather than as a strip above it: same grey as the
   page, no border under it. A border or a contrasting fill here is what makes a
   custom titlebar look bolted on.
   ========================================================================== */

.titlebar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Chrome of a known size, not a content-sized row, so `flex-shrink: 0` is
     load-bearing: without it the flex column compresses the bar when the shell
     is over-full, and it collapsed from 40px to 20px before this was added.
     Back to 40px now that the logo in it is no longer doubled. */
  height: 40px;
  flex: 0 0 auto;
  padding-left: var(--sp-4);
  background: transparent;
  position: relative;
  z-index: 10;
  user-select: none;
  -webkit-user-select: none;
}

.titlebar__brand {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  color: var(--text);
}

.titlebar__logo {
  /* Left at its original size. The doubling applied to the app's own logos - the
     one on the dashboard and the sign-in page - not to this small mark beside
     the name, which is window chrome rather than branding. */
  height: 20px;
  width: auto;
  display: block;
}

.titlebar__name {
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.titlebar__controls {
  display: flex;
  align-items: stretch;
  align-self: stretch;
}

/* ==========================================================================
   Window controls
   ==========================================================================
   Sized to fill the titlebar's height so the hit area is the full corner, the
   way native apps behave: 46px wide against Windows' ~46x32 default, and taller.
   ========================================================================== */

.toolbtn {
  font: inherit;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font-size: 16px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background .12s, color .12s;
}

.toolbtn svg {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
}

.toolbtn:hover {
  background: rgba(15, 23, 42, .07);
  color: var(--text);
}

/* The sort listbox in the header matches those buttons: same metrics, no border,
   a hover wash. The same component appears in the ticket form, where a bordered
   field is correct, so this is scoped to the header rather than changed on the
   shared `.cs__button`. */
.topbar .cs__button {
  border: none;
  background: transparent;
  color: var(--text-muted);
  /* Matches `.toolbtn`, so the three controls in that row align as one set
     rather than the middle one sitting a size smaller. */
  font-size: 16px;
  font-weight: 600;
  padding: 7px 8px 7px 10px;
  gap: var(--sp-2);
}

.topbar .cs__button:hover {
  background: rgba(15, 23, 42, .07);
  color: var(--text);
}

.topbar .cs__button[aria-expanded='true'] {
  background: rgba(15, 23, 42, .07);
  color: var(--text);
}

/* ==========================================================================
   Header
   ==========================================================================
   Three centred rows, as the original site had: the logo, then the search field,
   then the action buttons. The window chrome lives in the titlebar above, so
   nothing here doubles as a drag handle.

   Because the rows are centred and wrap on their own, no width breakpoint is
   needed - the layout already degrades by stacking rather than by squeezing.
   ========================================================================== */

.topbar {
  background: transparent;
  border-bottom: none;
  position: static;
  /* Solid space before the summary strip. The header's background is transparent,
     so without this the cards butted against the buttons and the two read as one
     block. */
  margin-bottom: var(--sp-6);
}

/* Masthead: just the logo, centred, with the control rows beneath it.
   It is a control, not decoration - clicking it returns the page to its default
   state (see `goHome` in js/app.js) - so it gets the pointer cursor and a focus
   ring, the two things that mark a control out from a picture.

   The ring is drawn on the image rather than on the link, because the link is a
   page-wide block around a centred logo: an outline on that box would be a
   rectangle across the whole header instead of around the thing the operator is
   looking at. It is inset so it never bleeds into the titlebar or the search
   field. */
.masthead {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Top keeps the logo clear of the titlebar; the bottom is the gap down to the
     search field, which is the one the eye reads as "logo, then search". */
  padding: var(--sp-5) var(--sp-4) var(--sp-6);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}

/* `--focus-ring` is deliberately a no-op app-wide, so this is a ring of its own.
   `currentColor` keeps it legible in both themes: it is the body text colour,
   near-black on the light grey and near-white on the dark one. */
.masthead:focus-visible {
  outline: none;
}

.masthead:focus-visible img {
  outline: 2px solid currentColor;
  outline-offset: -2px;
  border-radius: var(--radius);
}

.masthead img {
  height: 172px;
  width: auto;
  display: block;
}

/* Each row is centred as a unit - nothing is pushed to the edges. */
.topbar__row {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding-bottom: var(--sp-4);
}

/* A little more air under the search than between the buttons themselves, so the
   field and the actions below it read as two separate rows. */
.topbar__row--actions {
  padding-top: var(--sp-2);
  padding-bottom: 0;
}

/* --------------------------------------------------------------------------
   Header action buttons
   --------------------------------------------------------------------------
   Quieter than the app's primary buttons. "Dodaj" and "Zarada" are destinations,
   not the main action on this screen - the list itself is - so a solid blue fill
   twice in the header shouted over the content. Borderless with a hover wash
   reads as a toolbar and lets the search field lead.
   -------------------------------------------------------------------------- */

.topbar__row--meta {
  padding-bottom: var(--sp-3);
  gap: var(--sp-4);
}

/* Search field and its "Traži" button read as one centred unit. */
.searchrow {
  display: flex;
  align-items: stretch;
  gap: var(--sp-2);
  width: 100%;
  max-width: 720px;
}

.searchrow .search {
  flex: 1 1 auto;
  max-width: none;
}

/* The buttons row is centred too, so it sits under the search rather than
   spreading across the page. */
.topbar .toolbar {
  justify-content: center;
  margin-block: 0;
  gap: var(--sp-3);
}

/* The old inline logo block, kept for the gate which still uses it. */
.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  text-decoration: none;
  color: inherit;
  flex-shrink: 0;
}

.brand img {
  height: 40px;
  width: auto;
  display: block;
}

.brand__name {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
}

.brand__sub {
  display: block;
  font-size: 11px;
  color: var(--text-faint);
  font-weight: 400;
}

.topbar__spacer {
  flex: 1 1 auto;
}

/* --------------------------------------------------------------------------
   Search
   -------------------------------------------------------------------------- */

.search {
  position: relative;
  flex: 1 1 320px;
  max-width: 460px;
}

/* Search field: a restrained rounded rectangle, not a pill. It sits closer to
   the "Traži" button in shape, so the pair reads as one control. */
.search input {
  width: 100%;
  font: inherit;
  padding: 10px 14px 10px 38px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  transition: border-color .15s;
}

.search input:focus {
  outline: none;
  border-color: var(--border-strong);
}

.search__icon {
  position: absolute;
  left: 13px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-faint);
  pointer-events: none;
  width: 16px;
  height: 16px;
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

.btn {
  font: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 10px 16px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--green-600);
  color: #fff;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background .15s, box-shadow .15s, transform .05s;
}

.btn:hover {
  background: var(--green-700);
}

.btn:active {
  transform: translateY(1px);
}

.btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.btn:disabled {
  opacity: .55;
  cursor: not-allowed;
}

/* The icons on the form's action bar.
 *
 * Shown at every width. On a phone the label sits under the glyph; on a desktop
 * the button is a row and the glyph sits before the word, which is what `.btn`'s
 * own `gap` and `align-items: center` already arrange. Sized like the other modal
 * glyphs and `flex: none` so a long label cannot squash them. */
.btn__icon {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
}

.btn--blue {
  background: var(--blue-500);
}

.btn--blue:hover {
  background: var(--blue-600);
}

.btn--ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--border-strong);
}

.btn--ghost:hover {
  background: var(--bg);
}

.btn--danger {
  background: var(--red-500);
}

.btn--danger:hover {
  background: #d32f2f;
}

.btn--sm {
  padding: 7px 12px;
  font-size: 14px;
}

/* --------------------------------------------------------------------------
   Toolbar row: one aligned line, replacing the original's wrapped inline-blocks
   -------------------------------------------------------------------------- */

.toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-block: var(--sp-5);
}

/* --------------------------------------------------------------------------
   Select
   -------------------------------------------------------------------------- */

.select {
  font: inherit;
  padding: 10px 34px 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 12px center;
  appearance: none;
  cursor: pointer;
}

.select:focus {
  outline: none;
  border-color: var(--border-strong);
}

/* --------------------------------------------------------------------------
   Summary strip
   -------------------------------------------------------------------------- */

.stats {
  display: grid;
  /* `auto-fit` with a 140px floor put five cards onto two rows on a narrower
     window, which looked broken. 120px keeps all five on one row down to about
     1000px; below that they wrap deliberately, which is correct. */
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}

.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-4);
}

.stat__label {
  font-size: 12px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: .04em;
}

.stat__value {
  font-size: 24px;
  font-weight: 600;
  margin-top: 2px;
}

/* A small line under a stat's value, for the one figure that needs its parts
   spelled out - "Naplaćeno" on Zarada, which is a sum of two things the rows
   beneath it report separately. Kept quiet: it is a reconciliation, not a
   headline. */
.stat__note {
  font-size: 11px;
  color: var(--text-faint);
  margin-top: 2px;
}

.stat--open .stat__value {
  color: var(--green-700);
}

.stat--waiting .stat__value {
  color: var(--amber-500);
}

/* Matches .pill--zavrsen, so the summary strip and the status column use the
   same colour for the same state. */
.stat--done .stat__value {
  color: var(--slate-500);
}

.stat--closed .stat__value {
  color: var(--red-500);
}

/* A highlighted figure in an otherwise neutral stat row - currently "Tekuća
   godina" on Zarada. Green matches the profit colouring used elsewhere. */
.stat--accent .stat__value {
  color: var(--green-800);
}

/* --------------------------------------------------------------------------
   Status pills
   -------------------------------------------------------------------------- */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 500;
  color: #fff;
  white-space: nowrap;
}

.pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: .8;
}

.pill--otvoren {
  background: var(--green-600);
}

.pill--na_cekanju {
  background: var(--amber-500);
}

.pill--zavrsen {
  background: var(--slate-500);
}

.pill--zatvoren {
  background: var(--red-500);
}

.pill--nepoznat {
  background: var(--text-faint);
}

/* Clickable status cell */
.status-cell {
  position: relative;
  display: inline-block;
}

.status-cell > .pill {
  cursor: pointer;
  border: none;
  font-family: inherit;
}

.status-cell > .pill:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.status-menu {
  position: absolute;
  z-index: 60;
  top: calc(100% + 6px);
  left: 0;
  min-width: 160px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-1);
  display: none;
}

.status-menu[data-open='true'] {
  display: block;
}

.status-menu button {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  font: inherit;
  font-size: 14px;
  text-align: left;
  padding: 8px 10px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text);
}

.status-menu button:hover {
  background: var(--bg);
}

.status-menu .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   Table (desktop)
   -------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.table-wrap {
  overflow-x: auto;
}

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;

  /* Fixed layout is what keeps the columns in the same place.
     With the default `auto` layout the browser sizes each column to its widest
     cell, so the columns shift as you page or sort - "Opis" gets wider when a
     long description appears and every other column moves with it. Fixed layout
     takes the widths from the header row only and ignores the content. */
  table-layout: fixed;

  /* No minimum: the point is that the table fits whatever space it is given, so
     it never needs a horizontal scrollbar. The column widths below always sum to
     100%, which makes that hold at any width. */
}

table.data thead th {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  text-align: left;
  /* Nine columns at 14px each side is ~250px of pure gutter, which is what kept
     the table's intrinsic minimum above the width it was given and forced a
     horizontal scrollbar. 9px still reads comfortably and buys back ~90px. */
  padding: 11px 9px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
  /* Keep the declared width exact; without this the padding adds to it and the
     row overflows its own table. */
  box-sizing: border-box;
}

/* Column widths, declared once here rather than inline on the header cells, so
   the total is easy to verify. They sum to 100%.

   The three flexible columns carry the slack: name and model can be clipped
   mid-word without losing meaning, and `opis` gets the largest share because it
   is the field the serviser actually reads. */
/* These must sum to 100 - `table-layout: fixed` scales any excess down
   proportionally, so a set that adds to 107 silently renders Ime narrower than
   the 20% it claims. Measured, not assumed.

   Minimums come from measuring the rendered headings at 12px uppercase:
   INTERNO 58px, ZAPRIMLJENO 92px, TELEFON 60px - plus 18px of cell padding.
   INTERNO at 4.5% gave 51px against the 76px it needs, which is why the column
   read "INTERN". */
table.data th:nth-child(1) { width: 5.2%; }  /* ID       */
table.data th:nth-child(2) { width: 10.8%; } /* Ime      */
table.data th:nth-child(3) { width: 10.5%; } /* Telefon  */
table.data th:nth-child(4) { width: 11%; }   /* Model    */
table.data th:nth-child(5) { width: 25%; }   /* Opis     */
table.data th:nth-child(6) { width: 15%; }   /* Datum    */
table.data th:nth-child(7) { width: 11%; }   /* Status   */
table.data th:nth-child(8) { width: 6.5%; }  /* Interno  */
/* The hover reveal is a narrow gutter, not a full column: it only has to hold a
   30px pencil, so the table gives back as little as possible. */
table.data th:nth-child(9) { width: 5%; }    /* Uredi    */

/* The edit gutter holds a single icon, centred, so it needs no right padding -
   padding there would only squeeze the columns that carry information. */
table.data th:nth-child(9),
table.data td:nth-child(9) {
  padding-right: 0;
}

/* The heading is centred over its icon. Body cells are left-aligned by default,
   and a left-aligned "EDIT" over a centred icon was the visible misalignment. */
table.data th:nth-child(9) {
  text-align: center;
}

table.data tbody td {
  padding: 11px 9px;
  border-bottom: 1px solid var(--row-divider);
  vertical-align: middle;
  box-sizing: border-box;
}

/* Only the ID column gets the wider left inset, so it lines up with the "ID"
   heading. The other columns keep the tighter 9px from the shorthand above.

   Declared AFTER that shorthand on purpose: `padding` resets all four sides, so
   this has to come later to win. Putting it first silently did nothing - the
   cells kept the 9px and the change looked applied. */
table.data tbody td:first-child {
  padding-left: var(--sp-4);
}

table.data th:nth-child(1) {
  padding-left: var(--sp-4);
}

table.data tbody tr:last-child td {
  border-bottom: none;
}

table.data tbody tr:hover {
  background: var(--surface-3);
}

/* Truncate long free text, but let the user see all of it on demand. */
/* The description and name can be long, so they are clipped to the column rather
   than allowed to widen it (fixed layout ignores content for sizing, but the
   text still has to be told to truncate instead of spilling). */
.cell-clip {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cell-desc {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
}

.cell-num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cell-id {
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   Internal checkbox as a switch
   -------------------------------------------------------------------------- */

.switch {
  position: relative;
  display: inline-block;
  /* Centres the track on the line box instead of aligning its bottom edge to the
     text baseline.
   *
   * As an inline-block with the default `baseline`, its *bottom margin edge* sits
   * on the baseline, which parked the 22px track 3px above the optical centre of
   * the row - clear in the ticket table, where it did not line up with the status
   * pill beside it. `middle` aligns the box's centre with the parent's baseline
   * plus half an x-height, which is what every surrounding element uses.
   *
   * Irrelevant inside a flex container (the ticket form), where flex alignment
   * governs and this is ignored. */
  vertical-align: middle;
  width: 38px;
  height: 22px;
  flex-shrink: 0;
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.switch__track {
  position: absolute;
  inset: 0;
  background: var(--border-strong);
  border-radius: var(--radius-pill);
  transition: background .18s;
  pointer-events: none;
}

.switch__track::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  background: var(--switch-knob);
  border-radius: 50%;
  box-shadow: var(--shadow-sm);
  transition: transform .18s;
}

.switch input:checked + .switch__track {
  background: var(--green-600);
}

.switch input:checked + .switch__track::after {
  transform: translateX(16px);
}

.switch input:focus-visible + .switch__track {
  box-shadow: var(--focus-ring);
}

/* --------------------------------------------------------------------------
   Pagination
   -------------------------------------------------------------------------- */

/* ==========================================================================
   Pagination
   ==========================================================================
   Sized and shaped like a toolbar control rather than a web link: fixed square
   tiles, a heavy weight, and a filled active state. No underline, no hover
   text-decoration, no rounded-pill shape - those are what made it read as a
   web page.
   ========================================================================== */

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  flex-wrap: wrap;
  margin-block: var(--sp-5) var(--sp-3);
}

.pagination button {
  /* A fixed box, not a padded label: every tile is identical no matter how many
     digits the number has, so the row never shifts as you page. */
  width: 44px;
  height: 40px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;

  font: inherit;
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;

  /* Square-ish rather than rounded, which is the strongest signal that this is
     an application control and not a link. */
  border: 1px solid var(--green-700);
  border-radius: 3px;
  background: var(--green-600);
  color: #fff;
  cursor: pointer;
  user-select: none;

  /* Flat, by request: no inset highlight, no inner edge. The fill and the
     border carry the shape on their own. */
  box-shadow: none;
  transition: background .1s, border-color .1s;
}

.pagination button:hover {
  background: var(--green-700);
}

/* Pressed. The darker fill is the whole feedback; no inner shadow. */
.pagination button:active {
  background: var(--green-800);
}

/* The page you are on: a straight swap to the darkest green. No inset shading,
   so the selected state is distinguishable by fill alone. */
.pagination button[aria-current='page'] {
  background: var(--green-800);
  border-color: var(--green-800);
  cursor: default;
}

/* The gaps: a flat grey tile, sized to match so the row stays even. */
.pagination__gap {
  width: 44px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  box-shadow: none;
  color: var(--text-faint);
  font-weight: 700;
  user-select: none;
}

@media (max-width: 700px) {
  /* Narrow: the pager's controls shrink so the whole run of pages still fits one
     line. */
  .pagination {
    gap: 4px;
  }

  .pagination button,
  .pagination__gap {
    width: 38px;
    height: 36px;
    font-size: 14px;
  }
}

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-6);
  margin-block: var(--sp-5);
  box-shadow: var(--shadow-sm);
}

.field {
  margin-bottom: var(--sp-4);
}

.field > label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: 6px;
}

.input,
textarea.input,
select.input {
  font: inherit;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  transition: border-color .15s, box-shadow .15s;
}

/* No focus treatment on inputs: clicking one should not tint its border or add
   a glow. The caret is the only indication of where typing goes. */
.input:focus {
  outline: none;
  border-color: var(--border-strong);
}

textarea.input {
  resize: vertical;
  min-height: 96px;
}

.row-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
}

.form-actions {
  display: flex;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-top: var(--sp-5);
}

.form-actions .btn {
  flex: 1 1 auto;
}

/* --------------------------------------------------------------------------
   Damage flag checkboxes
   -------------------------------------------------------------------------- */

/* ==========================================================================
   Damage checklist
   ==========================================================================
   Six selectable tiles rather than six bare checkboxes.

   Each option is a tap target the size of a button, so the whole label toggles
   it rather than the 18px box, and the box itself becomes a status indicator:
   outlined when unset, filled green with a tick when set. The tile carries the
   selected state (border, tint and a faint inset ring), which makes the current
   damage report readable at a glance instead of requiring six checkbox reads.

   This matches the language already used elsewhere in the app - the intern
   switch, the status pills - so the form does not look like a web page.
   ========================================================================== */

.checks {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-2);
}

.check {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 14px;
  line-height: 1.25;
  cursor: pointer;
  user-select: none;
  transition: border-color .13s, background .13s, box-shadow .13s;
}

.check:hover {
  border-color: var(--border-strong);
  background: var(--bg);
}

/* The tick box. A flex child rather than an absolutely-positioned overlay, so a
   long label wraps under the text rather than under the box. */
.check input {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin: 0;
  flex: 0 0 auto;
  border: 2px solid var(--border-strong);
  border-radius: 5px;
  background: transparent;
  cursor: pointer;
  position: relative;
  transition: background .13s, border-color .13s;
}

.check input:checked {
  background: var(--green-600);
  border-color: var(--green-600);
}

/* The tick, drawn with a rotated border rather than a glyph so it does not
   depend on the brand font having a check character. */
.check input:checked::after {
  content: '';
  position: absolute;
  left: 6px;
  top: 2px;
  width: 4px;
  height: 9px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.check input:focus-visible {
  outline: none;
  border-color: var(--blue-500);
}

/* Selected tile. `:has()` lets the parent reflect its checkbox without any JS,
   which keeps the markup a plain label + input. */
.check:has(input:checked) {
  border-color: var(--green-600);
  background: var(--green-50);
  box-shadow: inset 0 0 0 1px var(--green-600);
}

.check:has(input:checked):hover {
  background: var(--check-checked-hover);
}

/* A count so the summary on the printed nalog is visible while filling it in. */
.damage-count {
  color: var(--green-700);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   Messages & toasts
   -------------------------------------------------------------------------- */

.alert {
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-sm);
  margin-bottom: var(--sp-4);
  font-size: 14px;
}

/* The three inline notices. Their washes are pale enough that they would glare
   on a dark surface, and their borders are the same hue at a darker step, so both
   are tokens: light mode keeps the original pastels, dark mode swaps to a deep
   tint of the same colour. */
.alert--ok {
  background: var(--alert-ok-bg);
  color: var(--green-800);
  border: 1px solid var(--alert-ok-border);
}

.alert--err {
  background: var(--alert-err-bg);
  color: var(--red-700);
  border: 1px solid var(--alert-err-border);
}

.alert--info {
  background: var(--alert-info-bg);
  color: var(--alert-info-ink);
  border: 1px solid var(--alert-info-border);
}

.toasts {
  position: fixed;
  right: var(--sp-5);
  bottom: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  z-index: 200;
  max-width: 360px;
}

.toast {
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  font-size: 14px;
  background: var(--toast-bg);
  color: var(--toast-ink);
  animation: toast-in .18s ease-out;
}

.toast--err {
  background: var(--red-700);
}

.toast--ok {
  background: var(--green-800);
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   Sync indicator
   -------------------------------------------------------------------------- */

.sync {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: 13px;
  color: var(--text-muted);
  white-space: nowrap;
}

.sync__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-faint);
  flex-shrink: 0;
}

.sync__dot--ok {
  background: var(--green-600);
}

.sync__dot--busy {
  background: var(--blue-500);
  animation: pulse 1s ease-in-out infinite;
}

.sync__dot--err {
  background: var(--red-500);
}

@keyframes pulse {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: .3;
  }
}

/* --------------------------------------------------------------------------
   Modal
   -------------------------------------------------------------------------- */

.modal {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, .45);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 300;
  padding: var(--sp-4);
}

.modal[data-open='true'] {
  display: flex;
}

/* ==========================================================================
   The change-password form
   ==========================================================================
   The modal centres its text, which is right for the title and the note under
   it, and wrong for a form: a label centred above its input reads as a caption
   rather than the name of a field. The form therefore resets the alignment and
   stacks its fields.
   ========================================================================== */

.password-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 16px;
  text-align: left;
}

.password-form .field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Shared label and hint styles. They did not exist before this form, so they are
   named generally rather than bolted onto the password modal alone. */
.field__label {
  font-size: 13px;
  font-weight: 600;
  opacity: 0.75;
}

.field__hint {
  font-size: 12px;
  opacity: 0.6;
}

.password-form .input {
  width: 100%;
  box-sizing: border-box;
}

.password-form .modal__actions {
  display: flex;
  gap: 8px;
  margin-top: 4px;
}

.password-form .alert {
  margin-top: 2px;
}

.modal__box {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-6);
  max-width: 420px;
  width: 100%;
  text-align: center;
}

.modal__title {
  font-size: 17px;  font-weight: 600;
  margin: 0 0 var(--sp-2);
}

.modal__body {
  color: var(--text-muted);
  margin-bottom: var(--sp-5);
  font-size: 14px;
}

.modal__actions {
  display: flex;
  gap: var(--sp-3);
}

.modal__actions .btn {
  flex: 1;
}

/* --------------------------------------------------------------------------
   Empty & loading states
   -------------------------------------------------------------------------- */

.empty {
  text-align: center;
  padding: var(--sp-6) var(--sp-4);
  color: var(--text-muted);
}

.empty__title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 4px;
}

/* --------------------------------------------------------------------------
   Zarada: an unreadable search phrase
   --------------------------------------------------------------------------
   Shown in the report's own list slot when the phrase in the search box is not a
   period the backend can read. It replaces the report rather than sitting above
   one, because the failure being reported is precisely that an unfiltered report
   used to be drawn as though it were the answer to the phrase.
   -------------------------------------------------------------------------- */

.earnings-note {
  border: 1px solid var(--amber-500);
  border-left-width: 3px;
  background: var(--amber-50);
  border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-4) var(--sp-4) var(--sp-3);
  max-width: 62ch;
}

.earnings-note__title {
  font-weight: 600;
  color: var(--text);
  margin-bottom: var(--sp-1);
}

.earnings-note__body {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-muted);
}

.skeleton {
  background: linear-gradient(90deg, #eef1f4 25%, #f6f8fa 50%, #eef1f4 75%);
  background-size: 200% 100%;
  animation: sk 1.2s infinite;
  border-radius: 4px;
  height: 14px;
}

@keyframes sk {
  from {
    background-position: 200% 0;
  }

  to {
    background-position: -200% 0;
  }
}

/* --------------------------------------------------------------------------
   Mobile: the dashboard becomes cards
   --------------------------------------------------------------------------
   The original forced a 9-column table with `min-width: 800px` and
   `white-space: nowrap`, so on a phone the most informative column (Opis) was
   clipped to an ellipsis and the whole thing needed horizontal scrolling.

   Below 1020px each ticket becomes a card instead. That threshold is set by
   measurement, not by device class: the nine-column table needs about 1000px of
   usable width to fit without a horizontal scrollbar, and anything narrower
   would either scroll sideways or squeeze the columns until values wrapped.
   -------------------------------------------------------------------------- */

.cards {
  display: none;
}

@media (max-width: 1020px) {
  :root {
    --gutter: 14px;
  }

  .table-wrap {
    display: none;
  }

  .cards {
    display: flex;
    flex-direction: column;
    /* Tighter than the desktop grid: at this width the list is the whole screen,
       so the space between tickets should read as a seam, not a gap. */
    gap: 8px;
  }

  /* A ticket on a phone.
   *
   * What this replaced measured 256px tall per ticket, of which most was air: a
   * three-row Telefon/Model/Serijski definition list, a footer holding the interno
   * switch and a full-width "Uredi" button, and a description that wrapped as far
   * as it liked. Four tickets fitted on a screen and the rest was whitespace.
   *
   * It is now two lines of text with the fault as an optional third, and the edit
   * pencil in the corner - the same pencil the table uses, because it is the same
   * action. Everything that was here and is not any more is one tap away in the
   * form this card opens. */
  /* The frame around the list goes flat.
   *
   * Every ticket is already its own card, so an outer frame here was a box drawn
   * around boxes - which is why the mobile list read as one white slab with seams
   * in it rather than as tickets. On a phone the list is the whole screen; there
   * is nothing for a frame to separate it from. */
  .card {
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .ticket-card {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    background: var(--surface);
    /* A card again: the outline all the way round, on a white surface. The left
       edge is the exception - it is thicker and takes the status colour, so the
       state of a ticket is readable without spending a pill's worth of width on
       every row. */
    border: 1px solid var(--border);
    border-left: 3px solid var(--ticket-status, var(--border));
    border-radius: var(--radius);
    /* 3px of left border plus 9px of padding keeps the text where it was when the
       edge was 1px, so nothing shifts sideways. */
    padding: 9px 8px 9px 9px;
    box-shadow: none;
  }

  /* Status as a colour down the left edge.
   *
   * The pill was taken off these cards because a phone list exists to be scanned,
   * and a word plus a dot on every row is a lot of ink to read past. A stripe says
   * the same thing in the one place the eye already is, and costs no width at all.
   *
   * The colours are the pill's own, so the list, the table and the summary strip
   * all say "Otvoren" in the same green. They come from theme variables, so they
   * follow the light and dark palettes without a second set of values. */
  .ticket-card--otvoren    { --ticket-status: var(--green-600); }
  .ticket-card--na_cekanju { --ticket-status: var(--amber-500); }
  .ticket-card--zavrsen    { --ticket-status: var(--slate-500); }
  .ticket-card--zatvoren   { --ticket-status: var(--red-500); }
  .ticket-card--nepoznat   { --ticket-status: var(--text-faint); }

  /* The tappable part, and a real <button> rather than a click listener on the
   * card: it is a much larger target than the pencil, and it is focusable and
   * announced, which a div with a click handler is not. */
  .ticket-card__open {
    flex: 1 1 auto;
    /* A flex item will not shrink below its content without this, which is what
     * had the old card sitting 52px wider than the window it was in. */
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
  }

  /* Both text lines clip rather than wrap: a ticket is one row of a list, and a
   * row that grows to three lines because one model name is long is exactly the
   * problem this layout exists to avoid. */
  .ticket-card__name {
    font-weight: 600;
    font-size: 15px;
    line-height: 1.35;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .ticket-card__facts {
    font-size: 12.5px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The separator is drawn rather than typed, so it cannot end up doubled or
   * left dangling when a field is empty. */
  .ticket-card__facts > span + span::before {
    content: '·';
    margin: 0 5px;
    color: var(--text-faint);
  }

  .ticket-card__desc {
    font-size: 12.5px;
    color: var(--text-faint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The table gives this glyph a column of its own and a header to explain it; on
   * a card it is just a corner, so it needs a target big enough for a thumb and
   * enough padding that it does not crowd the name. Negative margin pulls the
   * icon's optical centre back to the card's edge without shrinking the target. */
  .ticket-card__edit {
    flex: none;
    margin: -3px -2px 0 0;
    padding: 6px;
  }

  /* The actions become a bar of icon-over-word buttons.
   *
   * They were ordinary buttons that wrapped onto as many lines as the width
   * allowed, which on a phone meant two or three ragged rows at the bottom of a
   * long form. One row of equal columns with a glyph over each word is both
   * shorter and easier to hit: the whole column is the target, not just the text.
   *
   * `flex-start` rather than centring the content, so every icon lands on the same
   * line even when one label wraps to two lines and another does not. The buttons
   * themselves stay equal height because `.form-actions` is a flex row and they
   * stretch by default. */
  .form-actions {
    flex-wrap: nowrap;
    gap: 6px;
    margin-top: var(--sp-4);
  }

  .form-actions .btn {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    justify-content: flex-start;
    gap: 3px;
    padding: 9px 5px;
    font-size: 12px;
    line-height: 1.2;
    /* The base rule forbids wrapping, which is what made these overflow their
       share of the row rather than break onto a second line. */
    white-space: normal;
    text-align: center;
  }

  .form-actions .btn__icon {
    /* A little larger than the desktop 17px: stacked over the word rather than
       beside it, it reads as the button's face and needs the weight. */
    display: block;
    width: 20px;
    height: 20px;
  }

  .form-actions .btn__label {
    /* Long words in a narrow column, rather than a label poking out of its
       button. "printaj" is the longest thing here and fits, but the labels are
       shop copy and may change. */
    overflow-wrap: anywhere;
  }

  .brand__name {
    display: none;
  }

  .search {
    max-width: none;
    flex-basis: 100%;
  }

  .row-2,
  .checks {
    grid-template-columns: 1fr;
  }

  /* On a phone the status dropdown and the interno switch share one row.
   *
   * They answer the same question - what state is this ticket in - and as two
   * full-width rows they cost twice the height for one short dropdown and one
   * switch. `placeFormStateControls` in js/app.js is what puts the status field in
   * here; on anything wider it is moved back to the row with the parts price and
   * this rule stops applying.
   *
   * `flex-start`, and both fields given the same shape rather than the switch being
   * aligned after the fact. Bottom-aligning it put its centre 21px below the
   * select's, because the select sits at the bottom of a field that also holds a
   * "Status" label. Making the interno a label over a control too - the word moves
   * from beside the switch to above it - makes both fields the same height and the
   * two controls start at the same y, with no magic offsets. */
  #fieldInterno {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-4);
  }

  #fieldInterno > .field {
    flex: 0 1 auto;
    min-width: 0;
    /* The row owns the spacing now. A bottom margin here is counted in the
       container's height and drags the other child down with it. */
    margin-bottom: 0;
  }

  #fieldInterno .check--inline {
    /* `display` has to be restated: inside a `.field` this label is a direct child,
       so `.field > label` (0,1,1) beats `.check` (0,1,0) and it lays out as a
       block with inline children - which is why the rule below needs the ID to
       have any effect, and why `flex-direction` alone did nothing. */
    display: flex;
    /* Column-reverse lifts the word above the switch without moving it in the
       markup, which the desktop form still needs beside it. */
    flex-direction: column-reverse;
    align-items: flex-start;
    /* The same 6px `.field > label` uses to separate a label from its control. */
    gap: 6px;
  }

  /* Matching `.field > label`, so the two label lines are the same height and the
     two controls below them start level. */
  #fieldInterno .check--inline > span:last-child {
    font-size: 13px;
    font-weight: 500;
    line-height: 19.5px;
    color: var(--text-muted);
    /* Undoes the half-pixel and the 22px line box the inline layout needed. */
    top: 0;
  }

  .form-card {
    padding: var(--sp-4);
  }

  /* A `flex-direction: column` stood here, stacking the actions as one full-width
     button per row. It is a row of icon-over-word buttons now - see the rules
     earlier in this block - so it is deleted rather than left in place to win on
     source order. Deleting it is the whole fix: nothing else about the bar needs to
     know which shape it is. */
}

/* --------------------------------------------------------------------------
   Print - the servisni nalog
   -------------------------------------------------------------------------- */

@media print {
  body {
    background: #fff;
  }

  .no-print {
    display: none !important;
  }

  .form-card,
  .card {
    border: none;
    box-shadow: none;
    padding: 0;
    margin: 0;
  }
}

/* ==========================================================================
   Views
   --------------------------------------------------------------------------
   Two top-level views live in one document: the dashboard and the ticket form.
   Switching is a class toggle, so there is no router, no page reload, and the
   local data is never re-fetched just to show a form.
   ========================================================================== */

/* Switching between the top-level views.
   
   One rule set controls both kinds of view, and it is deliberately specific
   (`.page` on the element, not a bare class selector) so nothing else can
   out-rank it. An earlier revision used a plain `.view` selector, which lost
   the cascade and left the sign-in gate rendered underneath the dashboard at
   the same time - both were "block" at once. */
.page,
.view,
main.centered {
  display: none;
}

.page.view--active,
.view.view--active,
main.centered.view--active {
  display: block;
}

/* The gate is a flex column rather than a block, so it needs its own pair of
   rules at ID specificity. A lower-specificity hide rule would lose to the
   flex one and leave the sign-in screen stacked above the dashboard. */
#loginView {
  display: none;
}

/* The gate must fill what is left of the shell, or it centres its card inside a
   box that is only as tall as the card - which parks the card at the top. The
   scroll host sizes itself to its content here, so `flex: 1` on this view is what
   gives the gate a full-height parent to centre within.
   `min-height: 0` lets it shrink rather than push the titlebar off on a short
   window; the card then scrolls, which is the right behaviour. */
#loginView.view--active {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* --------------------------------------------------------------------------
   Servisni nalog (the printed receipt)
   --------------------------------------------------------------------------
   The original positioned every label and value with inline
   `position: relative; right: 145px` hacks - roughly forty of them across
   add.php and edit.php. Those offsets broke as soon as a value grew longer
   than the hard-coded number, so a long customer name would overlap its label.

   This is a real two-column grid instead: the same visual result, but values
   wrap naturally and nothing needs hand-tuning.
   -------------------------------------------------------------------------- */

.receipt {
  font-family: 'SNPro', Arial, sans-serif;
  color: #000;
  background: #fff;
}

.receipt__head {
  text-align: center;
  margin-bottom: 14px;
}

.receipt__head h1 {
  font-size: 18px;
  margin: 0 0 4px;
}

.receipt__head p {
  margin: 1px 0;
  font-size: 13px;
}

.receipt__title {
  text-align: center;
  margin: 14px 0 2px;
  font-size: 22px;
  letter-spacing: .04em;
}

.receipt__id {
  text-align: center;
  margin: 0 0 14px;
  font-size: 15px;
}

.receipt__section {
  font-weight: 600;
  font-size: 15px;
  border-bottom: 1.5px solid #000;
  padding-bottom: 3px;
  margin: 16px 0 10px;
}

.receipt__grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  border: 1px solid #000;
  border-bottom: none;
}

.receipt__grid > div {
  padding: 6px 10px;
  font-size: 13px;
  border-bottom: 1px solid #000;
}

.receipt__grid > .k {
  font-weight: 600;
  border-right: 1px solid #000;
  white-space: nowrap;
}

.receipt__damage {
  display: grid;
  grid-template-columns: max-content 1fr max-content 1fr;
  border: 1px solid #000;
}

.receipt__damage > div {
  padding: 5px 10px;
  font-size: 13px;
  border-bottom: 1px solid #000;
}

.receipt__damage > .k {
  font-weight: 600;
  border-right: 1px solid #000;
  white-space: nowrap;
}

.receipt__damage > .v {
  border-right: 1px solid #000;
  font-weight: 600;
}

.receipt__box {
  border: 1px solid #000;
  padding: 8px 10px;
  min-height: 54px;
  font-size: 13px;
  white-space: pre-wrap;
}

.receipt__signatures {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  margin-top: 46px;
}

.receipt__sig {
  flex: 1;
  text-align: center;
  font-size: 11px;
}

.receipt__sig-line {
  border-top: 1px solid #000;
  margin-top: 26px;
}

.receipt__note {
  font-size: 10px;
  line-height: 1.6;
  margin-top: 16px;
  border-top: 1px solid #000;
  padding-top: 8px;
}

/* When printing from either view, the servisni nalog is the only thing that
   appears. Everything marked `no-print` (toolbars, the form, the table) is
   hidden, the dashboard is hidden outright, and the receipt is forced visible
   even though it is normally collapsed on screen. */
@media print {
  .no-print,
  .view:not(.view--print),
  #dashboardView {
    display: none !important;
  }

  #formView {
    display: block !important;
    padding: 0 !important;
    margin: 0 !important;
    max-width: none !important;
  }

  .receipt-wrap {
    display: block !important;
    box-shadow: none !important;
    border: none !important;
    padding: 0 !important;
    margin: 0 !important;
  }
}

@media (max-width: 600px) {
  .receipt__damage {
    grid-template-columns: max-content 1fr;
  }
}

/* ==========================================================================
   Earnings
   ==========================================================================
   Year Ă˘â€ â€™ month Ă˘â€ â€™ week drill-down. The original used green year headers, grey
   month rows and a profit bar per week; that visual language is kept, but the
   collapse animation is now a real height transition driven by measured
   scrollHeight on the element rather than an inline style written from JS with
   magic numbers.
   ========================================================================== */

.earnings {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.year-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

.year-row,
.month-row {
  display: grid;
  /* One title, then five figures: Naplaćeno, Interno, Regularno, Bruto, Neto.
     The title column gives up the space rather than the figures - it holds a
     year or a month name, which is short, while the five beside it hold money
     and must not be the ones that wrap. */
  grid-template-columns: minmax(84px, 1fr) repeat(5, minmax(72px, 1fr));
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) var(--sp-4);
  cursor: pointer;
  user-select: none;
}

.year-row {
  background: var(--green-600);
  color: #fff;
}

.year-row:hover {
  background: var(--green-700);
}

.month-row {
  background: var(--surface-2);
  border-top: 1px solid var(--border);
}

.month-row:hover {
  background: var(--bg);
}

.year-row:focus-visible,
.month-row:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  position: relative;
  z-index: 1;
}

.col {
  min-width: 0;
}

.col-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-weight: 600;
}

.col .label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
  opacity: .75;
}

.col .value {
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   Profit highlight
   ==========================================================================
   Profit is the figure the shop actually looks for, so it is marked as a filled
   chip rather than left as another number in another column.

   Pale green fill with dark green text - not green-on-green. The year row is
   already solid green, so a green chip on it was near-invisible; measured, the
   first attempt gave white text on #388e3c against a #43a047 row, which read as
   one flat block. A light chip punches out of the green row and still reads as
   a green badge on the white month rows and the grey week rows. One rule, three
   backgrounds, legible on all of them.
   ========================================================================== */

.col--profit {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Both figures keep the weight - they are the two the shop reads - but the green
   now marks Neto rather than Bruto. Two green columns side by side left the eye
   to guess which one had the costs taken off it; the one worth picking out is
   what is actually left. */
.col--profit .value,
.col--net .value {
  font-weight: 700;
}

.col--net .value {
  color: var(--green-800);
}

/* A month that spent more than it took is the one case worth colouring red. It
   is only applied to a negative net, so an ordinary month never shows it. */
.col--net .value--neg {
  color: var(--red-500);
}

/* The year row keeps its figures white, because its background is solid green,
   and drops the extra weight: it is a summary of the months already shown above
   it, and at 700 it competed with the year label itself. */
.year-row .col--profit .value,
.year-row .col--net .value {
  color: #fff;
  font-weight: 400;
}

/* The label under the profit figure sits on the row's own background, so it
   keeps the inherited colour and opacity. */
.col--profit .label,
.col--net .label {
  padding-left: 2px;
}

.col .value .sub {
  display: block;
  font-size: 12px;
  opacity: .75;
}

.percent {
  font-size: 12px;
  opacity: .8;
  margin-left: 4px;
}

/* Disclosure caret */
.caret {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  transition: transform .18s ease;
}

.year-row[aria-expanded='true'] .caret,
.month-row[aria-expanded='true'] .caret {
  transform: rotate(90deg);
}

/* Collapsible containers. `max-height` is set from the measured content height
   so the transition has real numbers to animate between. */
.month-container,
.week-container {
  overflow: hidden;
  max-height: 0;
  transition: max-height .22s ease;
}

.week-container {
  background: var(--surface);
}

/* Week rows share the month row's column grid and horizontal padding so the
   amounts stack in the same vertical line. The week label is indented instead
   (see `.col-title` below), so the period reads as nested without pushing the
   figures sideways. */
.week-row {
  display: grid;
  /* Six tracks to match the month and year rows above, so the columns line up
     down the whole report. A week fills only five of them: costs are recorded
     per month and cannot be attributed to one week of it, so the last cell is
     left empty rather than carrying a share of the rent it did not earn. */
  grid-template-columns: minmax(84px, 1fr) repeat(5, minmax(72px, 1fr));
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px dashed var(--border);
  font-size: 14px;
}

/* The week's own label is nudged in, aligned with the month's text after its
   caret, so the nesting is visible without moving the numbers. */
.week-row .col-title {
  padding-left: 20px;
}

.week-row .value {
  font-variant-numeric: tabular-nums;
}

/* Profit bar: share of the week's price that was kept. */
.profit-bar {
  grid-column: 1 / -1;
  height: 6px;
  border-radius: 3px;
  background: var(--surface-4);
  overflow: hidden;
  margin-top: 2px;
}

.profit-fill {
  height: 100%;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--green-600), #66bb6a);
  transition: width .3s ease;
}

.profit-bar--loss .profit-fill {
  background: linear-gradient(90deg, var(--red-500), #ef7b72);
}

/* Summary strip above the list */
.earnings-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}

/* ==========================================================================
   "Prosjeci" panel
   ==========================================================================
   Replaces the two loose sentences the original printed here. An average on its
   own says very little - "39,13 KM per repair" means nothing without knowing how
   many repairs, or how it relates to the takings. Each tile therefore pairs a
   value with the context that makes it readable, and the tiles are grouped by
   the question they answer.
   ========================================================================== */

/* Separate from the year list above it. The list's own gap only spaces the year
   cards from each other, so without this the panel sat flush against the oldest
   year.

   A margin alone proved too weak: the year cards are ~60px tall with 12px
   between them, so 32px - and even 48px - read as "another item in the list"
   rather than a new section. The break is now structural: a labelled rule above
   the panel, with the margin reduced to a supporting role.
   -------------------------------------------------------------------------- */
.avg-section {
  margin-top: var(--sp-6);
  margin-bottom: var(--sp-6);
}

.avg-section__rule {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* No negative inline margin: this used to pull itself out by the panel's own
     padding to line up with a card edge, but the heading now sits directly in
     `#earningsContent`, so that pull left the text hanging to the left of
     everything under it. */
  margin: 0 0 var(--sp-4);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The label sits hard left and a single rule fills the space to its right, so
   the heading reads as the start of a section rather than a centred page title.
   Only `::after` is drawn - the leading rule would push the label off the left
   edge, which is where it belongs. */
.avg-section__rule::before {
  content: none;
}

.avg-section__rule::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
  /* The rule must sit on the text's centre line and stop short of it. Without
     `align-self` it stretches to the flex line's height and reads as a divider
     under the word rather than a line running out from it. */
  align-self: center;
  margin-left: var(--sp-2);
}

/* A tick box used on its own inside a form, rather than as one tile in the
   damage grid. `.check` gives it the padded, bordered tile look, which reads as
   a text input when there is only one - so the box is stripped back to a plain
   row and only the layout is kept.
 *
 * No vertical padding: the row is exactly as tall as what it holds, so
 * `align-items: center` from `.check` has nothing to centre against and the text
 * sits on the switch's centre line. Padding here pushed the text down instead. */
.check--inline {
  justify-content: flex-start;
  align-items: center;
  gap: var(--sp-2);
  width: fit-content;
  padding: 0;
  border: none;
  background: transparent;
  line-height: 1;
  cursor: pointer;
}

.check--inline:hover,
.check--inline:has(input:checked),
.check--inline:has(input:checked):hover {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

/* Direct child only: the switch inside also has an `input`, and sizing it here
   would fight `.switch input`, which must fill its 38x22 track to stay clickable. */
.check--inline > input {
  width: 18px;
  height: 18px;
}

/* Centring the label text on the switch.
 *
 * Two separate problems, and both had to be fixed for this to look right.
 *
 * 1. Height. Left alone the label is a 14px line box inside a 22px row, so
 *    `align-items: center` centres the box while the glyphs sit low in it. Giving
 *    the text the switch's own 22px line box makes both items the same height.
 *
 * 2. The remaining 1.5px. Even with the boxes level, the glyphs sit high inside
 *    the line box, so the ink lands above the pill's centre. Mapping the rendered
 *    pixels of the running window: the pill occupied rows 460-481 (centre 470.5)
 *    and the "Interno" glyphs rows 465-473 (centre 469). With `top: 0.5px` the
 *    glyph centre measures 470.5 - level.
 *
 * `position: relative`, NOT `transform`. The span is a flex item under
 * `align-items: center`, so the container decides its position and a transform is
 * ignored: an earlier `transform: translateY(...)` changed nothing at all, which
 * is why three different values all looked identical. Relative positioning
 * applies. */
.check--inline > span:last-child {
  line-height: 22px;
  position: relative;
  top: 0.5px;
}

/* When the row holds a switch rather than a tick box, it only needs to sit
   against the switch, not reserve the height of a checkbox. */
.check--inline > .switch {
  margin-right: 2px;
}

/* ==========================================================================
   Section heading with a rule
   ==========================================================================
   The "Zarada" page title, styled the same way as the "PROSJECI" divider further
   down: a small uppercase label on the left with a hairline running out to its
   right. Matching them makes the two pages' headings read as the same kind of
   thing rather than one being a title and the other a label.

   The lock button is a flex item at the end, so the rule stretches to fill the
   gap and stops short of the button. `.toolbar__spacer` is gone as a result -
   the rule does that job now.
   ========================================================================== */

.section-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-5);
}

.section-head__label {
  margin: 0;
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The rule. `flex: 1` makes it take all the slack.
 *
 * `order: 0` on the pseudo-element and `order: 1` on the button is what puts the
 * line *between* the label and the button. `::after` is always the last flex item
 * in source order, so without this the button sat immediately after the label and
 * the rule ran off to the right of it - the opposite of the intent. */
.section-head::after {
  content: '';
  order: 0;
  flex: 1;
  height: 1px;
  background: var(--border);
}

.section-head .btn {
  order: 1;
}

/* Hidden while the report is locked, and `hidden` must win over the flex item
   display - without this the button would still occupy space and leave a gap in
   the rule before it is unlocked. */
.section-head .btn[hidden] {
  display: none;
}

.avg-panel {  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-5);
}

.avg-group + .avg-group {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
}

.avg-group__label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin-bottom: var(--sp-2);
}

.avg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-2);
}

.avg-tile {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
}

.avg-tile__value {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  line-height: 1.2;
}

.avg-tile__caption {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  margin-top: 2px;
}

/* The "what this number means" line. Quiet, so the value still leads. */
.avg-tile__note {
  font-size: 11px;
  color: var(--text-faint);
  margin-top: 2px;
}

/* An absent figure: no value, just the reason. */
.avg-tile--empty {
  grid-column: 1 / -1;
  background: transparent;
  border-style: dashed;
}

.avg-tile--empty .avg-tile__note {
  margin-top: 0;
  font-size: 12px;
  color: var(--text-muted);
}

@media (max-width: 720px) {

  /* Two columns of figures per row, with each figure stacked over its label.
     An earlier attempt put the label *beside* the value to save height, but at
     this width a figure like "28.919 KM" then wrapped mid-number, which is far
     worse than the extra vertical space. Value over label it is. */
  .year-row,
  .month-row,
  .week-row {
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-2) var(--sp-3);
    padding-inline: var(--sp-3);
  }

  .week-row {
    padding-left: var(--sp-3);
    padding-right: var(--sp-3);
  }

  .week-row .col-title {
    padding-left: 0;
  }

  /* The period name spans the full width. */
  .col-title {
    grid-column: 1 / -1;
  }

  /* Keep any figure on one line; scroll rather than wrap if it is tight. */
  .year-row .col .value,
  .month-row .col .value,
  .week-row .col .value {
    white-space: nowrap;
  }
}

/* ==========================================================================
   Earnings unlock gate
   ==========================================================================
   The report is gated on the account password. This is not a second credential
   — it is the same password used to sign in — but it means an unattended,
   already-signed-in machine does not expose the shop's revenue figures.
   ========================================================================== */

.unlock {
  display: flex;
  justify-content: center;
  padding-block: var(--sp-6);
}

.unlock__box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-6);
  max-width: 420px;
  width: 100%;
  text-align: center;
}

.unlock__icon {
  width: 46px;
  height: 46px;
  margin: 0 auto var(--sp-4);
  color: var(--text-faint);
}

.unlock__icon svg {
  width: 100%;
  height: 100%;
}

.unlock__title {
  font-size: 19px;
  margin: 0 0 var(--sp-2);
}

.unlock__hint {
  color: var(--text-muted);
  font-size: 14px;
  margin: 0 0 var(--sp-5);
}

.unlock__form {
  display: flex;
  gap: var(--sp-2);
  align-items: stretch;
}

.unlock__form .input {
  flex: 1 1 auto;
}

.unlock__form .btn {
  flex: 0 0 auto;
}

@media (max-width: 520px) {
  .unlock__box {
    padding: var(--sp-4);
  }

  .unlock__form {
    flex-direction: column;
  }
}

/* ==========================================================================
   Sign-in gate
   ==========================================================================
   The whole app sits behind this. It is the only thing rendered until the
   server accepts the credentials, so there is no way to reach the dashboard,
   a ticket or the earnings report without a session.
   ========================================================================== */

.gate {
  /* Fills what is left after the custom titlebar rather than the whole viewport.
     `100vh` here overflowed by the bar's height, which pushed a scrollbar onto
     the sign-in screen. */
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5);
}

.gate__box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--sp-6);
  width: 100%;
  max-width: 400px;
  text-align: center;
}

.gate__logo {
  /* Sized to read as the card's mark rather than dominate it. The full logo
     doubled to 236px turned the sign-in form into a logo with some fields under
     it - the fields are the point of this screen. */
  width: 84px;
  height: auto;
  margin-bottom: var(--sp-4);
}

.gate__title {
  font-size: 22px;
  margin: 0 0 2px;
}

.gate__subtitle {
  color: var(--text-muted);
  font-size: 14px;
  margin: 0 0 var(--sp-5);
}

/* "Zapamti korisnika": a plain checkbox row, left-aligned with the fields above
   it. `.check` is borrowed for the checkbox itself, but its padded, bordered
   "tile" styling belongs to the damage checklist in the ticket form - here it
   would read as a third input field, so the box and background are stripped. */
.gate__remember {
  justify-content: flex-start;
  gap: var(--sp-2);
  padding: 0;
  margin-bottom: var(--sp-4);
  border: none;
  background: transparent;
  font-size: 14px;
  color: var(--text-muted);
  cursor: pointer;
}

/* Flat in every state: idle, hovered, and hovered-while-ticked.
 *
 * All of them have to be restated because `.check`, `.check:hover` and
 * `.check:has(input:checked):hover` each set their own background. The last is
 * (0,4,0) - four class-level parts - so the `label` element here is what lifts
 * this selector to (0,4,0) as well. Equal specificity plus coming later in the
 * file is what makes it win; without the element it is (0,3,0) and loses, which
 * is exactly the tint that showed up when the box was ticked and hovered. */
label.gate__remember,
label.gate__remember:hover,
label.gate__remember:has(input:checked),
label.gate__remember:has(input:checked):hover {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

.gate__remember input {
  width: 17px;
  height: 17px;
}

/* The warning under the certificate opt-in. Deliberately not styled as muted
/* Read-only facts in the settings dialog. */
.settings-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--sp-2) var(--sp-4);
  margin: var(--sp-4) 0;
  font-size: 14px;
}

.settings-facts dt {
  color: var(--text-muted);
  white-space: nowrap;
}

.settings-facts dd {
  margin: 0;
  font-weight: 500;
}

/* ==========================================================================
   Custom select
   ==========================================================================
   A native <select> renders as the operating system's own control, which looks
   out of place next to the rest of the interface and cannot be styled beyond
   the closed state (the popup list is drawn by the OS).

   This is a real listbox: a button plus an absolutely-positioned panel. It
   handles click, keyboard, Escape and outside-click, and keeps the ARIA roles
   a screen reader needs.
   ========================================================================== */

.cs {
  position: relative;
  display: inline-block;
  min-width: 0;
  /* The width is measured from the widest option in JS and applied here, so the
     control keeps a constant size whatever is selected. Sizing it to the current
     label made it - and everything beside it in the centred header row - jump
     whenever a shorter option was chosen. */
  width: max-content;
}

.cs--block {
  display: block;
  width: 100%;
}

.cs__button {
  font: inherit;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  text-align: left;
  transition: border-color .15s, box-shadow .15s;
}

/* The open state is shown by the rotated caret. No focus ring at all, by
   request - the panel opening is already clear feedback. */
.cs__button:hover {
  border-color: var(--text-faint);
}

.cs__button[aria-expanded='true'] {
  border-color: var(--border-strong);
}

.cs__button:focus,
.cs__button:focus-visible {
  outline: none;
  box-shadow: none;
}

.cs__button:focus:not(:focus-visible) {
  outline: none;
  box-shadow: none;
}

.cs__value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cs__caret {
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  color: var(--text-muted);
  transition: transform .18s ease;
}

.cs__button[aria-expanded='true'] .cs__caret {
  transform: rotate(180deg);
}

.cs__menu {
  position: absolute;
  z-index: 120;
  top: calc(100% + 4px);
  left: 0;
  min-width: 100%;
  max-height: 280px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-1);
  display: none;
}

.cs__menu[data-open='true'] {
  display: block;
}

.cs__option {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  cursor: pointer;
  white-space: nowrap;
  color: var(--text);
}

.cs__option:hover,
.cs__option[data-active='true'] {
  background: var(--bg);
}

.cs__option[aria-selected='true'] {
  font-weight: 600;
  color: var(--green-700);
}

.cs__option .cs__tick {
  margin-left: auto;
  width: 14px;
  height: 14px;
  color: var(--green-600);
  flex-shrink: 0;
}

/* Status colour dot inside a status option */
.cs__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* The native selects are kept in the markup as the source of option labels but
   are replaced visually by the custom listbox, so they must not take space. */
select[hidden] {
  display: none !important;
}

/* --------------------------------------------------------------------------
   Form modal
   --------------------------------------------------------------------------
   Add and edit share one overlay. It scrolls internally so a long form works
   on a short screen, and it is deliberately NOT clipped by `overflow: hidden`
   anywhere, because the custom dropdown panels have to be able to extend past
   it.
   -------------------------------------------------------------------------- */

.modal--sheet {
  align-items: flex-start;
  padding: var(--sp-5) var(--sp-4);
  overflow-y: auto;
}

.sheet {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 680px;
  margin: auto;
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - (2 * var(--sp-5)));
}

.sheet__head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.sheet__title {
  font-size: 18px;
  margin: 0;
  flex: 1 1 auto;
}

.sheet__close {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.sheet__close:hover {
  background: var(--bg);
  color: var(--text);
}

.sheet__close svg {
  width: 17px;
  height: 17px;
}

.sheet__body {
  padding: var(--sp-5);
  overflow-y: auto;
}

.sheet__body .form-actions {
  position: sticky;
  bottom: calc(-1 * var(--sp-5));
  background: var(--surface);
  padding-block: var(--sp-3);
  margin-bottom: calc(-1 * var(--sp-3));
  border-top: 1px solid var(--border);
}

@media (max-width: 600px) {
  .modal--sheet {
    padding: 0;
  }

  .sheet {
    max-width: none;
    max-height: 100vh;
    min-height: 100vh;
    border-radius: 0;
  }
}

/* ==========================================================================
   Servisni nalog (printed receipt)
   ==========================================================================
   These class names and measurements are copied from the ORIGINAL add.css so
   the printed page is the document the shop already uses: a 4-column box grid
   (.info-grid-25), a 2-column one (.info-grid-50), and a signature row.

   The old stylesheet built the two-column "label | value" appearance out of
   alternating bordered boxes, then nudged each value into place with an inline
   `position: relative; right: 145px`. That offset only worked for short values.
   Here the same visual result comes from a borderless value box; the label and
   value still occupy two of the four grid cells, so the printed geometry is
   unchanged but nothing depends on a magic number.
   ========================================================================== */

#receipt {
  font-family: 'SNPro-Regular', 'SNPro', Arial, Helvetica, sans-serif;
  color: #000;
  background: #fff;
}

#receipt .header,
#receipt .section {
  text-align: center;
  margin-bottom: 16px;
}

/* No logo on the receipt. The original PHP app carried one in its page header,
   but the print stylesheet hides everything outside #printable, so it never
   reached paper - the reference PDF's only images are two renders of the
   uzorak.png signature grid. Matching that exactly. */

#receipt .header h1 {
  font-size: 18px;
  margin: 0 0 5px;
}

#receipt .header p {
  margin: 2px 0;
  font-size: 14px;
}

#receipt .section h2 {
  font-size: 30px;
  margin: 14px 0 0;
}

#receipt .section h3 {
  margin: 3px 0 0;
  font-size: 17px;
}

#receipt .section-title {
  font-weight: bold;
  font-size: 20px;
  margin: 16px 0 10px;
  text-align: left;
}

#receipt .info-grid-50 {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

#receipt .info-grid-50 .box {
  width: calc(42% - 5px);
  border: 1px solid #000;
  padding: 10px;
  font-size: 13px;
}

#receipt .info-grid-25 {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 10px;
}

#receipt .info-grid-25 .box {
  width: calc(21% - 7.5px);
  border: 1px solid #000;
  padding: 10px;
  font-size: 13px;
}

/* A value box carries the same footprint as a label box but without its own
   border, which is what makes the pair read as one cell. */
#receipt .box--value {
  border-left: none;
}

#receipt .box--label {
  border-right: none;
}

#receipt .box--wide {
  border-bottom: none;
  border-right: none;
}

#receipt .box-textarea {
  border: 1px solid #000;
  padding: 10px;
  min-height: 60px;
  margin-bottom: 20px;
  font-size: 14px;
  width: 100%;
  box-sizing: border-box;
  white-space: pre-wrap;
}

#receipt .signatures {
  display: flex;
  justify-content: space-between;
  margin-top: 60px;
}

#receipt .signature {
  width: 33%;
  text-align: center;
}

#receipt .signature p {
  font-size: 11px;
  margin: 0;
}

#receipt .signature-line {
  margin-top: 26px;
  border-top: 1px solid #000;
  width: 100px;
  margin-left: auto;
  margin-right: auto;
}

/* The sample-signature image sits above the M.P. line with a negative top
   margin, exactly as in the original. */
#receipt .signature--stamp {
  margin-top: -56px;
}

#receipt .signature-image {
  margin-bottom: 5px;
}

#receipt .signature-image img {
  width: 90px;
  height: 90px;
  object-fit: cover;
  display: block;
  margin: 32px auto 0;
}

#receipt .note {
  font-size: 9px;
  margin-top: 16px;
  line-height: 1.6;
}

/* --------------------------------------------------------------------------
   Print
   --------------------------------------------------------------------------
   Same approach as the original: hide everything, then reveal the receipt.
   `position: absolute; top: 0` plus a negative top margin matches the original
   page geometry so the printed layout is not shifted.
   -------------------------------------------------------------------------- */

@media print {
  body * {
    display: none !important;
  }

  #receiptWrap,
  #receiptWrap * {
    display: block !important;
  }

  #receiptWrap {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    padding: 20px;
    margin-top: -20px;
    border: none !important;
    box-shadow: none !important;
  }

  #receipt .box {
    border-bottom: none;
    border-right: none;
    display: inline;
    page-break-inside: avoid;
    break-inside: avoid;
  }

  #receipt .box span {
    white-space: nowrap;
  }

  /* Keep the flex containers as flex: `body *` above forced everything to
     `display: block`, which would otherwise stack every box vertically. */
  #receipt .info-grid-50,
  #receipt .info-grid-25 {
    display: flex !important;
    flex-wrap: wrap !important;
    gap: 0 !important;
    margin-top: 0;
  }

  #receipt .info-grid-50 .box {
    width: 49.4% !important;
    box-sizing: border-box !important;
  }

  #receipt .info-grid-25 .box {
    width: 24.7% !important;
    box-sizing: border-box !important;
  }

  #receipt .signatures {
    display: flex !important;
    justify-content: space-between !important;
    margin-top: 60px !important;
  }

  #receipt .signature {
    width: 43.2% !important;
    text-align: center !important;
  }
}

/* On screen the receipt is shown below the form, on white, readable. */
.receipt-wrap {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px;
  margin: var(--sp-5) auto;
  max-width: var(--content-narrow);
  box-shadow: var(--shadow-sm);
}

/* Footer of the dashboard: sign out and sync side by side, then the state and
   the version notes beneath. */
.page-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding-block: var(--sp-6);
  margin-top: var(--sp-5);
  border-top: 1px solid var(--border);
}

.page-foot__actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.page-foot .btn {
  min-width: 150px;
}

.version {
  color: var(--text-faint);
  text-decoration: none;
  font-size: 12px;
}

.version:hover {
  color: var(--text-muted);
}

/* The footer's closing row: the two quiet links and the theme switch. */
.page-foot__meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

/* ==========================================================================
   Theme switch
   ==========================================================================
   Small and quiet, so it reads as a footer control rather than a third button.
   Only one of the two icons is ever shown: the sun while the app is dark (the
   thing you would switch *to*), the moon while it is light.
   ========================================================================== */

.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-faint);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  transition: color .12s, border-color .12s, background .12s;
}

.theme-toggle:hover {
  color: var(--text);
  border-color: var(--border-strong);
  background: var(--surface);
}

.theme-toggle svg {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
}

/* Which icon shows. `aria-pressed` is the single source of truth here, so the
   icon, the label and the accessible state can never disagree. */
.theme-toggle__sun {
  display: none;
}

.theme-toggle__moon {
  display: block;
}

[data-theme='dark'] .theme-toggle__sun {
  display: block;
}

[data-theme='dark'] .theme-toggle__moon {
  display: none;
}

/* Below ~1080px the date column is dropped rather than allowed to force a
   horizontal scrollbar.
   
   A full "14-09-2026 11:32" needs ~118px, and that hard minimum was enough to
   push the whole table wider than its container. The list is sorted by date by
   default, so the order still tells you what you need; you just stop seeing the
   exact timestamp. It returns as soon as there is room. */
@media (max-width: 1080px) and (min-width: 1021px) {
  /* The date column is now index 6, since the action column moved to the end. */
  table.data th:nth-child(6),
  table.data td:nth-child(6) {
    display: none;
  }

  /* Seven columns visible (the date is hidden, the pencil gutter is index 9 and
     takes no declared width here because it is too narrow to matter at this
     size). Percentages sum to 100. */
  table.data th:nth-child(1) { width: 5.5%; }  /* ID       */
  table.data th:nth-child(2) { width: 15.5%; } /* Ime      */
  table.data th:nth-child(3) { width: 12%; }   /* Telefon  */
  table.data th:nth-child(4) { width: 20%; }   /* Model    */
  table.data th:nth-child(5) { width: 28%; }   /* Opis     */
  table.data th:nth-child(7) { width: 13%; }   /* Status   */
  table.data th:nth-child(8) { width: 6%; }    /* Interno  */
}

/* ==========================================================================
   "Only the receipt" state
   ==========================================================================
   Used for native printing. WebView2's `PrintToPdf` captures the page as laid
   out on screen, so the `@media print` rules do not apply and the app shell
   would otherwise be captured along with the receipt.

   The receipt is a CHILD of `.shell`, not a sibling, so hiding `.shell` outright
   hid the receipt too and the capture came out as a blank grey page. Hide the
   shell's other children instead, and lift the receipt out of the flow so it is
   the only thing on the page.
   ========================================================================== */

/* The header stays. It is the only thing that can move the window now that the
   native decorations are off, so hiding it during a print would trap the user in
   a window with no controls for as long as the PDF takes.
 *
 * No `.shell >` here. That child combinator was written when everything sat
 * directly inside the shell, and it silently broke printing when the scroll host
 * was introduced: `#receiptWrap` moved one level down, so this selector no longer
 * matched it and the `display: block !important` below could not win against
 * `display: none !important`. The result was a **blank PDF** - a page with
 * `/Length 0` and no content at all.
 *
 * Matching `#receiptWrap` by identity, at any depth, is what the rule actually
 * means: "hide everything except the receipt and the titlebar".
 *
 * Hiding a descendant is not enough. An element is invisible if ANY ancestor is
 * `display:none`, because the whole subtree leaves the box tree. The selector
 * below excluded the receipt and its descendants but NOT its ancestors, so
 * `#appShell` - which *contains* `#receiptWrap` - was itself hidden. The receipt
 * stayed `display:block` inside a hidden parent, measured 0x0, nothing was ever
 * painted, and the capture came back as a solid black frame of the app's own dark
 * background. Measured, not guessed:
 *
 *   receiptWrap[block 0x0]   <- applied, but inside a hidden parent
 *     appShell[none 0x0]     <- the real cause
 *       scroll-host[none 0x0]
 *         shell[flex 1280x860]
 *           printing-receipt[block 1280x860]
 *
 * `:has()` keeps every ancestor of the receipt alive, so the chain from `.shell`
 * down to `#receiptWrap` stays laid out while everything else is hidden. WebView2
 * is Chromium, which has had `:has()` since 105. */
body.printing-receipt .shell *:not(#receiptWrap):not(#receiptWrap *):not(:has(#receiptWrap)):not(.topbar):not(.topbar *) {
  display: none !important;
}

/* Modals and overlays sit inside the shell too; nothing should be painted over
   the receipt. */
body.printing-receipt #ticketModal,
body.printing-receipt #confirmModal,
body.printing-receipt #unlockModal,
body.printing-receipt .status-menu,
body.printing-receipt .toasts {
  display: none !important;
}

/* The receipt moves to the top of the page, unwrapped from the card styling, so
   the PDF is just the document. */
body.printing-receipt #receiptWrap {
  display: block !important;
  position: absolute;
  inset: 0 auto auto 0;
  width: 100%;
  margin: 0;
  padding: 20px;
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: #fff;
}

/* The shell itself must stay laid out - it is the containing block for the
   absolutely-positioned receipt above. */
body.printing-receipt .shell {
  min-height: 0;
}

/* ==========================================================================
   Notes + unlock pattern
   ==========================================================================
   The two free-text boxes take what is left, and the column on the right holds
   the unlock pattern with its PIN field beside it. Both sides stretch to the
   taller of the two, so the textareas grow to meet the pad rather than leaving
   a ragged edge.

   The right track is wide enough for the pad (220px) plus a usable input
   column, and no wider: every pixel it takes comes off the textareas. The
   upper bound matters because the sheet that holds this form is 680px wide, so
   a roomier track here is a narrower "Opis problema" there.
   ========================================================================== */

.notes-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

.notes-row__text {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-4);
  min-width: 0; /* lets the textareas shrink inside the grid track */
}

/* The textareas fill the column, so the writing area matches the pad's height
   instead of each box being its own awkward size. */
.notes-row__text .field {
  display: flex;
  flex-direction: column;
}

/* Both boxes the same height.
 *
 * They were 130px and 110px on purpose, on the theory that the fault
 * description deserves more room than the serviser's comment. Side by side it
 * only reads as a mistake - one box overflowing its neighbour. The shorter
 * height wins, so the row stays as compact as the two fields allow.
 *
 * `height` as well as `min-height`: a textarea has its own intrinsic size from
 * its `rows`, so the minimum alone can leave the two unequal anyway.
 */
.notes-row__text .field textarea {
  height: 110px;
  min-height: 110px;
}

.notes-row__pattern {
  display: flex;
  flex-direction: column;
  /* Everything in this column shares one left edge: the label, the pad, the
     hint and the button. Centring the pad on its own made the label and the
     hint start at a different place from the control they describe. */
  align-items: flex-start;
}

/* The pad on the left, everything that belongs beside it on the right.
   `auto` for the pad, because it is a fixed 220px square and the grid must not
   stretch it; the second track takes the rest. */
.pattern-body {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
  width: 100%;
}

/* The column beside the pad: the PIN field, then the pad's hint and its clear
   button - the three things that used to sit stacked under the canvas. */
.pattern-side {
  display: flex;
  flex-direction: column;
  /* Lets the input shrink inside the track instead of forcing the grid wider
     than the column it was given. */
  min-width: 0;
}

/* The three items in the side column are one group, so their spacing is tighter
   than the 16px a field gets on its own. */
.pattern-side .field {
  margin-bottom: var(--sp-3);
}

.pattern-side .pattern-help {
  margin: 0 0 var(--sp-3);
}

/* The pad is the reference width for the column. The label, the hint and the
   button all begin at its left edge, and the hint wraps at its right. */
.pattern-pad {
  width: 220px;
  max-width: 100%;
  aspect-ratio: 1 / 1;
  display: block;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  /* The pad is dragged, so the browser must not treat the gesture as a scroll
     or a text selection. */
  touch-action: none;
  cursor: crosshair;
  user-select: none;
  -webkit-user-select: none;
}

.pattern-help {
  font-size: 12px;
  color: var(--text-faint);
  /* Beside the pad the column itself is the constraint, so the sentence wraps
     to the space it actually has rather than to a fixed 220px. */
  margin: var(--sp-2) 0 var(--sp-3);
}

.pattern-actions {
  display: flex;
  gap: var(--sp-2);
}

@media (max-width: 700px) {
  .notes-row {
    grid-template-columns: 1fr;
  }

  .notes-row__pattern {
    order: -1; /* the pad first, since it is the thing that needs a finger */
  }

  /* One column: the pad, then the PIN field full width underneath it. */
  .pattern-body {
    grid-template-columns: minmax(0, 1fr);
  }

  /* A wider pad on a touch screen. */
  .pattern-pad {
    width: 260px;
    max-width: 100%;
  }
}

/* ==========================================================================
   Printer picker
   ==========================================================================
   Rows rather than a <select>, because each option has to say what it will
   actually do - picking a virtual writer and expecting paper is the mistake
   this list exists to prevent.

   Styled to sit inside the standard .modal__box (which is 420px and centred),
   so the tiles are left-aligned text in a bordered row, matching the app's
   cards and list rows rather than inventing a new control.
   ========================================================================== */

.printer-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  text-align: left;
  max-height: 46vh;
  overflow-y: auto;
}

.printer-tile {
  font: inherit;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: 10px 12px;
  text-align: left;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  transition: border-color .12s, background .12s;
}

.printer-tile:hover {
  border-color: var(--green-600);
  background: var(--green-50);
}

.printer-tile__text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.printer-tile__name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  /* Long printer names are common ("Microsoft XPS Document Writer (Copy 1)");
     clip rather than let the row widen past the dialog. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.printer-tile__meta {
  font-size: 12px;
  color: var(--text-muted);
}

/* The icon carries the meaning: a printer for a real device, a download arrow
   for saving a file. Sized to match .unlock__icon's stroke language. */
.printer-tile__icon {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  color: var(--text-faint);
}

.printer-tile:hover .printer-tile__icon {
  color: var(--green-600);
}

.printer-tile--save .printer-tile__icon {
  color: var(--green-700);
}

/* The default printer gets a quiet left marker rather than a whole extra row. */
.printer-tile--default {
  border-color: var(--green-600);
}

.printer-tile--default .printer-tile__icon {
  color: var(--green-600);
}

/* "Save as PDF" never depends on hardware, so it is set apart from the device
   list with a dashed border and a little space above. */
.printer-tile--save {
  margin-top: var(--sp-2);
  border-style: dashed;
}

.printer-tile--save:hover {
  border-style: solid;
}

.printer-empty {
  color: var(--text-muted);
  font-size: 14px;
  margin: 0;
  padding: 10px 0;
  text-align: center;
}

.printer-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin: var(--sp-4) 0 0;
  text-align: center;
  line-height: 1.45;
}

/* A warning tint: this message means nothing will print to paper. */
.printer-hint--warn {
  color: #b45309;
  font-weight: 500;
}

/* ==========================================================================
   Sheet views
   ==========================================================================
   The ticket dialog holds two views: the edit form and the printer picker.
   Printing swaps one for the other in place, so the operator never ends up with
   a dialog stacked on a dialog. `data-view` on `.sheet` decides which is shown;
   the form is the default so nothing has to set it on first open.
   ========================================================================== */

.sheet__view--print {
  display: none;
}

.sheet[data-view='print'] .sheet__view--print {
  display: block;
}

/* The history panel, the third view of the same sheet. Same switch, same reason:
   a dialog stacked on a dialog reads as a mistake, and the form underneath keeps
   its state for free. */
.sheet__view--changes {
  display: none;
}

.sheet[data-view='changes'] .sheet__view--changes {
  display: block;
}

.sheet[data-view='changes'] .sheet__head,
.sheet[data-view='changes'] .sheet__body {
  display: none !important;
}

/* Everything that belongs to the form hides while the picker is up. The body
   holds the form, the message area and the action rows; the header still shows
   the ticket id, which is useful context, so only its title area is hidden by
   the picker's own heading taking over. */
.sheet[data-view='print'] .sheet__head,
.sheet[data-view='print'] .sheet__body {
  display: none !important;
}

/* The picker needs a back affordance, and its own heading - the sheet's usual
   header says "Uredi tiket #830", which would be wrong here. */
.printview__head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-4) var(--sp-5) 0;
}

.printview__body {
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  overflow-y: auto;
  min-height: 120px;
}

.printview__back {
  font: inherit;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  align-self: flex-start;
  margin-bottom: var(--sp-3);
  padding: 6px 12px 6px 8px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color .12s, color .12s, background .12s;
}

.printview__back:hover {
  border-color: var(--green-600);
  color: var(--green-700);
  background: var(--green-50);
}

.printview__back svg {
  width: 16px;
  height: 16px;
}

.printview__title {
  font-size: 17px;
  font-weight: 600;
  margin: 0;
  color: var(--text);
}

.printview__sub {
  font-size: 13px;
  color: var(--text-muted);
  margin: 2px 0 0;
}

/* --------------------------------------------------------------------------
   Izmjene - the ticket's recorded history
   --------------------------------------------------------------------------
   One card per save rather than one row per field. The log records a field at a
   time, all with the same `changed_at` second, so an edit that touched the price,
   the status and the comment is one event to the person reading it, not three.
   Grouping is done in JS; this is only what a group looks like.
   -------------------------------------------------------------------------- */

.changes {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.change-group {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.change-group__when {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  padding: 7px 12px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.change-group__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.change-row {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 4px var(--sp-3);
  align-items: baseline;
  padding: 8px 12px;
  font-size: 13.5px;
}

.change-row + .change-row {
  border-top: 1px solid var(--border);
}

/* A row that states something that happened rather than a field that changed -
   currently only "the ticket was reopened". The label column is dropped so the
   sentence spans the row and reads as a sentence. */
.change-row--event {
  grid-template-columns: 1fr;
}

.change-row--event .change-row__new {
  color: var(--green-700);
}

.change-row__field {
  color: var(--text-muted);
  font-weight: 500;
}

/* Old and new share a line while they fit and fall onto their own line when a
   comment is long, which is the common case for `opis` and `komentar`. */
.change-row__values {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  min-width: 0;
  overflow-wrap: anywhere;
}

.change-row__old {
  color: var(--text-faint);
  /* Struck through rather than merely dimmed: the two values are otherwise easy
     to read in the wrong order, and "which one is it now" is the whole question
     this panel answers. */
  text-decoration: line-through;
  text-decoration-color: var(--border-strong);
}

.change-row__arrow {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  color: var(--text-faint);
  align-self: center;
}

.change-row__new {
  color: var(--text);
  font-weight: 500;
}

/* A value that was empty is shown as a word, not as a gap: on this panel "was
   nothing, now 70" is the interesting direction and a blank on the left of the
   arrow reads as a rendering fault. */
.change-row__empty {
  color: var(--text-faint);
  font-style: italic;
  font-weight: 400;
}

.changes__state {
  padding: var(--sp-5) 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 14px;
}

.changes__state--err {
  color: var(--red-600);
}

.changes__spinner {
  display: inline-block;
  width: 15px;
  height: 15px;
  margin-right: 8px;
  vertical-align: -2px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--text-muted);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* On a phone the two columns become one, and the field name becomes a heading
   over its own values. 132px of a 390px screen is a third of it spent on the
   word "Komentar". */
@media (max-width: 700px) {
  .change-row {
    grid-template-columns: 1fr;
    gap: 1px;
  }

  .change-row__field {
    font-size: 11.5px;
    text-transform: uppercase;
    letter-spacing: .4px;
    color: var(--text-faint);
  }
}

/* ==========================================================================
   Row edit affordance
   ==========================================================================
   The "Uredi" button is gone from the row, which returns ~7% of the table width
   to the columns that actually carry information. The action now appears as a
   pencil at the right edge on hover.

   Kept in the DOM at all times rather than injected on hover: a button that only
   exists while the pointer is over the row cannot be reached by keyboard, and
   hover-then-click can race the render. It is simply made transparent and
   non-interactive until the row is hovered or something inside it has focus.
   ========================================================================== */

.cell-edit {
  padding-inline: 0;
  /* Centre the icon, both axes, rather than relying on the cell's own text
     alignment. A single inline button inside a table cell sits on the text
     baseline, which left it a couple of pixels high of the row's centre and out
     of step with the status pill beside it.

   `display: flex` on the cell does that directly; the `margin-inline: auto` on
   the button then has no effect, which is fine - `justify-content` replaces it. */
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ==========================================================================
   Row edit icon
   ==========================================================================
   Always visible now, with its own EDIT column heading. It started as a hover
   reveal, but a control that only exists while the pointer is over the row is
   invisible to anyone scanning the table, and the heading makes what the column
   is for explicit rather than something to discover.

   Borderless and blue: the row already carries a green status pill and a green
   Interno switch, so a third green control would compete. Blue is the app's
   "action" colour - the same one the Dodaj and Zarada buttons use.
   ========================================================================== */

.row-edit {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--blue-500);
  cursor: pointer;
  transition: background .12s, color .12s;
}

.row-edit svg {
  width: 17px;
  height: 17px;
}

.row-edit:hover {
  /* A translucent wash of the icon's own colour, so the tint stays in step if
     the blue is ever changed. There is no light-blue token in the palette. */
  background: rgba(33, 150, 243, .12);
  color: var(--blue-600);
}

.row-edit:focus-visible {
  outline: none;
  background: rgba(33, 150, 243, .12);
}

/* The whole row is a double-click target, so it should look clickable. */
tbody tr[data-row] {
  cursor: pointer;
}

tbody tr[data-row]:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--green-600);
}

/* ==========================================================================
   Window controls
   ==========================================================================
   The native Windows chrome is off (`"decorations": false`), so these three
   buttons are the window controls. They live in the `.topbar`, which is why
   that bar also carries `data-tauri-drag-region` - see the top of this file.

   Sized to fill the header's height so the hit area is the full corner, the way
   native apps behave: 46px wide against Windows' ~46x32 default, and taller.
   ========================================================================== */

/* The window buttons. Square-ish and filling the bar's height so the hit area is
   the full corner - the way native apps behave. */
.wbtn {
  appearance: none;
  width: 46px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background .12s, color .12s;
}

.wbtn svg {
  width: 12px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.2;
  stroke-linecap: round;
}

.wbtn:hover {
  background: rgba(15, 23, 42, .07);
  color: var(--text);
}

/* Close is the one control that turns red, so a mis-click is obvious before it
   lands rather than after. */
.wbtn--close:hover {
  background: var(--red-500);
  color: #fff;
}

/* Only one of the two maximise glyphs shows at a time. */
.wbtn__restore {
  display: none;
}

body.is-maximized .wbtn__max {
  display: none;
}

body.is-maximized .wbtn__restore {
  display: block;
}

/* --------------------------------------------------------------------------
   Accessibility helpers
   -------------------------------------------------------------------------- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* --------------------------------------------------------------------------
   Costs (Troškovi)
   -------------------------------------------------------------------------- */

/* Wider than the default modal: this one holds a form and a list of months,
   where the others hold a sentence and a button. */
.modal__box--wide {
  max-width: 560px;
  text-align: left;
}

.expense-form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin: 4px 0 10px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

/* The description takes whatever is left, so a long one is readable rather than
   clipped, and the two fixed fields keep their width. */
.expense-form #expenseDate   { flex: 0 0 148px; }
.expense-form #expenseDesc   { flex: 1 1 160px; min-width: 0; }
.expense-form #expenseAmount { flex: 0 1 110px; }

.expense-list {
  max-height: 44vh;
  overflow-y: auto;
}

/* One month's heading. A cost's month is the thing that decides which figures it
   changes, so it is worth making obvious rather than leaving the operator to read
   every date. */
.expense-month {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  margin: 14px 0 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.expense-month:first-child {
  margin-top: 2px;
}

.expense-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.expense-row:last-child {
  border-bottom: 0;
}

.expense-row__date {
  flex: 0 0 auto;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  /* The date is a fixed ten characters and reads as one thing. Left to wrap, a
     narrow column broke "2026-10-01" across two lines as "2026-10-" / "01",
     which looks like two dates. */
  white-space: nowrap;
}

.expense-row__desc {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.expense-row__amount {
  flex: 0 0 auto;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.expense-row__acts {
  flex: 0 0 auto;
  display: flex;
  gap: 2px;
}

.expense-empty {
  padding: 16px 0;
  color: var(--text-faint);
  text-align: center;
}

/* A phone: one field per line rather than three squeezed onto one. */
@media (max-width: 560px) {
  .expense-form #expenseDate,
  .expense-form #expenseDesc,
  .expense-form #expenseAmount {
    flex: 1 1 100%;
  }

  .expense-form .btn {
    flex: 1 1 auto;
  }

  .expense-row__date {
    font-size: 12.5px;
  }
}

/* ==========================================================================
   Phone header: the logo and a hamburger, with the rest inside a panel
   ==========================================================================
   What a phone had: the logo centred and 172px tall, then the search field and
   the three actions in centred rows under it. That is most of the first screen
   spent on things the shop touches once a session, and the action row was wide
   enough to wrap onto a second line by itself.

   What it has now: one row - the logo on the left, a hamburger on the right -
   and the search field, "Dodaj", the sort control and "Zarada" inside the panel
   that button opens.

   Nothing in this section applies above 700px, and `.topbar__panel` is given no
   styling at all outside the media query, so on a desktop it stays a plain
   wrapper and the header lays out exactly as it did before this section existed.
   ========================================================================== */

/* The hamburger. Not hidden with `visibility` or by moving it off screen: it is
   `display: none` at every width above the breakpoint, so it takes no space in
   the header and cannot be reached by Tab on a screen that has no use for it. */
.menubtn {
  display: none;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: background .12s, border-color .12s;
}

/* The pressed state is the open state - it is the same control either way, and
   `aria-expanded` is already the single source of truth for it. */
.menubtn:hover,
.menubtn[aria-expanded='true'] {
  background: var(--bg);
  border-color: var(--border-strong);
}

.menubtn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.menubtn svg {
  width: 20px;
  height: 20px;
}

@media (max-width: 700px) {
  /* One row across the top. With the panel taken out of flow, only the logo and
     the button are left in it, so `space-between` *is* the arrangement: logo
     hard left, hamburger hard right. */
  .topbar .centered {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    padding-block: var(--sp-3);
    /* The panel is positioned against this box, so `left`/`right` on it mean the
       content edges rather than the window's. */
    position: relative;
  }

  /* The masthead keeps its link and its job - it is still "go home" - and gives
     up only the centring and the height. A 172px logo on a phone was most of the
     first screen before the shop had read a single ticket. */
  .masthead {
    padding: 0;
    flex: 0 1 auto;
    min-width: 0;
  }

  .masthead img {
    height: 44px;
  }

  .menubtn {
    display: inline-flex;
  }

  /* The dropdown itself. Absolutely positioned so it overlays the summary strip
     instead of pushing the list down, and so opening it cannot move the page
     under the finger that opened it. */
  .topbar__panel {
    display: none;
    position: absolute;
    top: calc(100% + 2px);
    left: var(--gutter);
    right: var(--gutter);
    /* Above the strip and the list, below the modals at 300. The custom-select
       menu is 120, and it is a descendant of this element, so it still paints
       over the panel's own contents. */
    z-index: 150;
    flex-direction: column;
    gap: var(--sp-2);
    padding: var(--sp-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
  }

  .topbar__panel[data-open='true'] {
    display: flex;
  }

  /* Both rows fill the panel and stack, rather than being centred as units in a
     page-wide row. */
  .topbar__panel .topbar__row {
    padding: 0;
    justify-content: flex-start;
  }

  /* Left-aligned like a menu rather than centred like a toolbar, and with the
     controls allowed to wrap: at 360px "Dodaj" and the sort control fit on one
     line and "Zarada" takes the next, which is the whole reason this is a panel
     and not a header row. */
  .topbar__panel .toolbar {
    justify-content: flex-start;
    margin-block: 0;
    gap: var(--sp-2);
  }

  /* The total is the one tile the strip can do without on a phone: every other
     tile is a state the shop acts on, and this one cost a whole row. */
  .stats .stat--total {
    display: none;
  }
}


/* ==========================================================================
   The three actions, as menu entries
   ==========================================================================
   Inside the phone panel these read as a list of things to do rather than as a
   toolbar, so they lose the bold weight and take a size closer to a menu item.

   Scoped to the phone breakpoint: above it the same three controls are a
   toolbar across the top of the page, where the smaller, heavier treatment is
   what makes them read as controls rather than as text.

   The universal selector is deliberate. These three are a mix of a .toolbtn, a
   custom select built at runtime and a plain button, and their own rules set
   font-size with enough specificity to beat a class selector. Setting it on
   every descendant and letting inheritance do the work covers all three without
   this file needing to know how each one is built.
   ========================================================================== */
@media (max-width: 700px) {
  .topbar__row--actions,
  .topbar__row--actions * {
    font-weight: 400;
    font-size: 17px;
  }
}

/* ==========================================================================
   The ticket sheet on a phone: Status full width, Interno under it, and the two
   text boxes on a line each
   ==========================================================================
   At the END of the file deliberately, rather than with the other
   `@media (max-width: 700px)` blocks further up.

   Two of the three rules here override base rules that are written BELOW those
   other media blocks - `.notes-row__text` is at the bottom of the "Notes + unlock
   pattern" section, and `#fieldInterno`'s column layout is in the 1020px block.
   A media query carries no extra weight in the cascade, so a phone rule that
   comes first with the same specificity simply loses to the base rule that
   follows it. That is exactly how an earlier attempt at this - the very same
   `.notes-row__text` one-liner, placed near the pager - had no effect at all on
   the form. Keeping the overrides last means source order is on their side.
   ========================================================================== */

@media (max-width: 700px) {
  /* `placeFormStateControls` in js/app.js moves the status field into
     `#fieldInterno` below 1020px, so on a phone these two are already siblings,
     one after the other.

     The switch was the layout's only other child, which put Status beside it;
     that block's comment explains why sharing the row was worth doing. At 390px
     it is not: the custom dropdown is the widest control in the sheet and had
     been squeezed into half of it. Stacking restores it to full width and drops
     Interno immediately underneath, which is the order the two are asked in
     anyway. Desktop is untouched - this only exists below 700px. */
  #fieldInterno {
    flex-direction: column;
    /* Row spacing, in place of the block's `gap: var(--sp-4)`, which is the gap
       between two things standing side by side. A column wants the `margin` step
       every other field in this form already uses. */
    gap: var(--sp-4);
  }

  /* The field is a block already, so this is belt and braces: it states that the
     dropdown spans the sheet's content box rather than being sized to its own
     longest option ("Na Cekanju"), which `width: max-content` on `.cs` would
     otherwise do. `.cs--block` does say `width: 100%`, but the rule ordering
     between the two is not something a phone layout should rely on. */
  #fieldStatus,
  #fieldStatus .cs,
  #fieldStatus .cs__button {
    width: 100%;
  }

  /* One text box per line, so each gets the full width to write in. The `.field`
     children are stacked by the column set here; the textareas already fill
     their column. */
  .notes-row__text {
    grid-template-columns: minmax(0, 1fr);
  }
}


/* ==========================================================================
   The three actions: never bold
   ==========================================================================
   Dodaj, the sort dropdown and Zarada are controls, and the heavier weight the
   toolbar gave them made them read as headings next to the search field. 400
   everywhere, at every width.

   At the END of the file on purpose. The weight comes from rules further up -
   and from the buttons' own base styles - so this has to be the last word
   rather than a rule that a later one quietly overrides. The universal
   selector's specificity (0,1,1) already beats a bare `.toolbtn` (0,1,0); the
   position covers anything more specific still.

   The phone panel's own 17px stays where it is; only the weight is global.
   ========================================================================== */
.topbar__row--actions,
.topbar__row--actions * {
  font-weight: 400;
}


/* --------------------------------------------------------------------------
   Dodaj, the sort dropdown and Zarada: #454545 in light.
   Dark keeps the muted grey it has always used.

   ID selectors, and `!important`, deliberately. Four earlier attempts - a
   plain class rule, one scoped with :not([data-theme='dark']), an explicit
   [data-theme='dark'] override, and a per-theme custom property - all failed
   identically: the buttons computed to rgb(69,69,69) in BOTH themes, while the
   variable itself read correctly in each (#454545 / #9aa7b6) and the page
   background did change with the theme. So the theme works and the variable
   works, and something sets this colour with more authority than a class.
   An ID is the one thing a class cannot outrank. The tidier versions are in
   the history if the real cause ever turns up.
   -------------------------------------------------------------------------- */
#addBtn,
#earningsBtn,
#chatBtn,
#sortSelectMount,
#sortSelectMount * {
  color: #454545 !important;
}

[data-theme='dark'] #addBtn,
[data-theme='dark'] #earningsBtn,
[data-theme='dark'] #chatBtn,
[data-theme='dark'] #sortSelectMount,
[data-theme='dark'] #sortSelectMount * {
  color: var(--text-muted) !important;
}

/* ==========================================================================
   Poruke - the live chat from sandi.ba
   ==========================================================================
   Deliberately close to WhatsApp on a desktop, because that is the chat the
   shop already answers all day: conversations on the left, the open one on the
   right, green outgoing bubbles on a beige wall, ticks that turn blue when the
   other side has seen the message.

   The palette is its own set of tokens on `.wa` rather than the app's: these
   colours mean "a chat" to anyone who has used one, and the app's greys would
   make it look like a form. Dark mode redefines the same tokens, nothing else.
   ========================================================================== */

.wa {
  --wa-side: #ffffff;
  --wa-panel: #f0f2f5;
  --wa-hover: #f5f6f6;
  --wa-active: #f0f2f5;
  --wa-line: #e9edef;
  --wa-wall: #efeae2;
  --wa-wall-dot: rgba(0, 0, 0, .035);
  --wa-in: #ffffff;
  --wa-out: #d9fdd3;
  --wa-ink: #111b21;
  --wa-muted: #667781;
  --wa-accent: #00a884;
  --wa-badge: #25d366;
  --wa-tick: #53bdeb;
  --wa-chip: #ffffff;
  --wa-input: #ffffff;
  --wa-error: #ea0038;
}

[data-theme='dark'] .wa {
  --wa-side: #111b21;
  --wa-panel: #202c33;
  --wa-hover: #202c33;
  --wa-active: #2a3942;
  --wa-line: #222d34;
  --wa-wall: #0b141a;
  --wa-wall-dot: rgba(255, 255, 255, .025);
  --wa-in: #202c33;
  --wa-out: #005c4b;
  --wa-ink: #e9edef;
  --wa-muted: #8696a0;
  --wa-chip: #182229;
  --wa-input: #2a3942;
}

/* The header button's unread count. */
.chat-badge {
  min-width: 19px;
  height: 19px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: #25d366;
  color: #ffffff !important;
  font-size: 12px;
  font-weight: 700;
  line-height: 19px;
  text-align: center;
}

.chat-badge[hidden] {
  display: none;
}

/* A chat wants the window's height, so the header steps back while it is up:
   a small logo, and no ticket search - this page searches its own list. The
   full header returns with the dashboard. */
@media (min-width: 701px) {
  body[data-view='chat'] .masthead {
    padding: var(--sp-3) var(--sp-4) var(--sp-2);
  }

  body[data-view='chat'] .masthead img {
    height: 56px;
  }

  body[data-view='chat'] .topbar__row:has(#searchForm) {
    display: none;
  }

  body[data-view='chat'] .topbar {
    margin-bottom: var(--sp-3);
  }
}

/* For the chat to reach the bottom of the window, every box between it and the
   scrolling host has to pass the height down. Scoped to this page so the other
   pages keep the plain block flow they were built on. */
body[data-view='chat'] #appShell:not([hidden]) {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* The page takes whatever height the header leaves, so the message list - not
   the window - is what scrolls, as in any chat. */
#chatView.view--active {
  display: flex;
  flex: 1 1 auto;
  min-height: 440px;
  max-width: 1440px;
  padding-bottom: var(--sp-5);
}

.wa {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow);
  background: var(--wa-side);
  color: var(--wa-ink);
}

/* ---- left: the conversation list --------------------------------------- */

.wa__side {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-right: 1px solid var(--wa-line);
  background: var(--wa-side);
}

.wa__sidehead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: 14px 16px 8px;
}

.wa__title {
  margin: 0;
  font-size: 21px;
  font-weight: 700;
}

.wa__conn {
  font-size: 12px;
  color: var(--wa-error);
}

.wa__search {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 12px 8px;
  padding: 0 12px;
  height: 36px;
  border-radius: 8px;
  background: var(--wa-panel);
  color: var(--wa-muted);
}

.wa__search svg {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}

.wa__search input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  color: var(--wa-ink);
  font: inherit;
  font-size: 14px;
}

.wa__list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.wa__item {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  padding: 0 0 0 13px;
  border: none;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.wa__item:hover {
  background: var(--wa-hover);
}

.wa__item[aria-current='true'] {
  background: var(--wa-active);
}

.wa__itembody {
  flex: 1;
  min-width: 0;
  padding: 12px 15px 12px 0;
  border-bottom: 1px solid var(--wa-line);
}

.wa__row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}

.wa__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 16px;
}

.wa__time {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--wa-muted);
}

.wa__item--unread .wa__time {
  color: var(--wa-badge);
  font-weight: 600;
}

.wa__preview {
  flex: 1;
  min-width: 0;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
  color: var(--wa-muted);
}

.wa__preview .wa__ticks {
  margin-right: 3px;
  vertical-align: -2px;
}

.wa__unread {
  flex: 0 0 auto;
  min-width: 20px;
  height: 20px;
  margin-top: 2px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--wa-badge);
  color: #ffffff;
  font-size: 12px;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}

.wa__nolist {
  padding: 40px 24px;
  text-align: center;
  font-size: 14px;
  color: var(--wa-muted);
}

/* Initials on a colour picked from the conversation id, so a person keeps
   their colour from one day to the next. */
.wa__avatar {
  position: relative;
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: #ffffff;
  font-size: 18px;
  font-weight: 600;
  background: var(--wa-avatar, #6b7c85);
}

.wa__avatar--online::after {
  content: '';
  position: absolute;
  right: 0;
  bottom: 1px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--wa-badge);
  border: 2px solid var(--wa-side);
}

/* ---- right: the open conversation -------------------------------------- */

.wa__main {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background-color: var(--wa-wall);
  background-image: radial-gradient(var(--wa-wall-dot) 1.2px, transparent 1.3px);
  background-size: 18px 18px;
}

.wa__empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 40px;
  text-align: center;
  color: var(--wa-muted);
  background: var(--wa-panel);
  border-bottom: 6px solid var(--wa-accent);
}

.wa__empty svg {
  width: 72px;
  height: 72px;
  margin-bottom: 10px;
  opacity: .55;
}

.wa__empty h3 {
  margin: 0;
  font-size: 26px;
  font-weight: 300;
  color: var(--wa-ink);
}

.wa__empty p {
  margin: 6px 0 0;
  font-size: 14px;
  line-height: 1.5;
}

.wa__conv {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.wa__conv[hidden],
.wa__empty[hidden] {
  display: none;
}

.wa__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px 9px 16px;
  background: var(--wa-panel);
  border-bottom: 1px solid var(--wa-line);
}

.wa__head .wa__avatar {
  width: 40px;
  height: 40px;
  font-size: 16px;
}

.wa__headtext {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.wa__headtext strong {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 16px;
  font-weight: 600;
}

.wa__headtext span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  color: var(--wa-muted);
}

.wa__iconbtn {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--wa-muted);
  cursor: pointer;
}

.wa__iconbtn:hover {
  background: rgba(0, 0, 0, .06);
}

[data-theme='dark'] .wa__iconbtn:hover {
  background: rgba(255, 255, 255, .08);
}

.wa__iconbtn svg {
  width: 21px;
  height: 21px;
}

/* Only a phone needs a way back to the list; on a desktop the list is there. */
.wa__back {
  display: none;
}

.wa__msgs {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 14px 7% 10px;
  display: flex;
  flex-direction: column;
}

.wa__older {
  align-self: center;
  margin-bottom: 10px;
  padding: 6px 14px;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--wa-chip);
  color: var(--wa-muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  box-shadow: 0 1px .5px rgba(11, 20, 26, .13);
}

.wa__day {
  align-self: center;
  margin: 10px 0;
  padding: 5px 12px;
  border-radius: 8px;
  background: var(--wa-chip);
  color: var(--wa-muted);
  font-size: 12.5px;
  text-transform: uppercase;
  box-shadow: 0 1px .5px rgba(11, 20, 26, .13);
}

.wa__msg {
  position: relative;
  max-width: min(65%, 560px);
  margin-top: 2px;
  padding: 6px 7px 8px 9px;
  border-radius: 7.5px;
  background: var(--wa-in);
  color: var(--wa-ink);
  font-size: 14.2px;
  line-height: 19px;
  box-shadow: 0 1px .5px rgba(11, 20, 26, .13);
  align-self: flex-start;
  overflow-wrap: anywhere;
}

.wa__msg--out {
  align-self: flex-end;
  background: var(--wa-out);
}

/* The first bubble of a run gets the little tail and a bit more air above it,
   so a change of speaker reads at a glance. */
.wa__msg--first {
  margin-top: 10px;
}

.wa__msg--first.wa__msg--in {
  border-top-left-radius: 0;
}

.wa__msg--first.wa__msg--out {
  border-top-right-radius: 0;
}

.wa__msg--first::before {
  content: '';
  position: absolute;
  top: 0;
  width: 8px;
  height: 13px;
}

.wa__msg--first.wa__msg--in::before {
  left: -8px;
  background: var(--wa-in);
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}

.wa__msg--first.wa__msg--out::before {
  right: -8px;
  background: var(--wa-out);
  clip-path: polygon(0 0, 100% 0, 0 100%);
}

.wa__text {
  white-space: pre-wrap;
}

/* Time and ticks float at the end of the last line, as WhatsApp does: the
   invisible spacer reserves room so the text never runs under them. */
.wa__text::after {
  content: '';
  display: inline-block;
  width: var(--wa-meta-w, 52px);
}

.wa__msg--out .wa__text::after {
  --wa-meta-w: 70px;
}

.wa__meta {
  position: absolute;
  right: 7px;
  bottom: 3px;
  display: flex;
  align-items: center;
  gap: 3px;
  font-size: 11px;
  color: var(--wa-muted);
  white-space: nowrap;
}

.wa__msg--imgonly {
  padding: 3px;
}

.wa__msg--imgonly .wa__meta {
  right: 10px;
  bottom: 7px;
  padding: 1px 6px;
  border-radius: 8px;
  background: rgba(0, 0, 0, .35);
  color: #ffffff;
}

.wa__ticks {
  width: 16px;
  height: 11px;
  flex: 0 0 auto;
  color: var(--wa-muted);
}

.wa__ticks--read {
  color: var(--wa-tick);
}

.wa__msg--imgonly .wa__ticks {
  color: #ffffff;
}

.wa__msg--imgonly .wa__ticks--read {
  color: var(--wa-tick);
}

.wa__msg--pending .wa__meta svg {
  opacity: .8;
}

.wa__msg--failed {
  cursor: pointer;
}

.wa__failed {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  color: var(--wa-error);
}

.wa__imgs {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3px;
  margin-bottom: 4px;
}

.wa__imgs--multi {
  grid-template-columns: 1fr 1fr;
}

.wa__msg--imgonly .wa__imgs {
  margin-bottom: 0;
}

.wa__img {
  display: block;
  width: 100%;
  min-width: 140px;
  max-width: 330px;
  max-height: 330px;
  border: none;
  border-radius: 6px;
  padding: 0;
  overflow: hidden;
  background: rgba(0, 0, 0, .08);
  cursor: zoom-in;
}

.wa__img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Photos picked but not sent yet, above the composer. */
.wa__files {
  display: none;
  gap: 8px;
  padding: 10px 16px 0;
  background: var(--wa-panel);
}

.wa__files--on {
  display: flex;
}

.wa__chip {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--wa-chip);
}

.wa__chip img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.wa__chip button {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, .6);
  color: #ffffff;
  font-size: 14px;
  line-height: 20px;
  cursor: pointer;
}

.wa__compose {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 16px;
  background: var(--wa-panel);
}

.wa__compose textarea {
  flex: 1;
  min-width: 0;
  max-height: 140px;
  padding: 10px 12px;
  border: none;
  border-radius: 8px;
  outline: none;
  resize: none;
  background: var(--wa-input);
  color: var(--wa-ink);
  font: inherit;
  font-size: 15px;
  line-height: 20px;
}

.wa__compose textarea::placeholder {
  color: var(--wa-muted);
}

.wa__send {
  flex: 0 0 auto;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: var(--wa-accent);
  color: #ffffff;
  cursor: pointer;
}

.wa__send svg {
  width: 20px;
  height: 20px;
}

.wa__send:disabled {
  opacity: .5;
  cursor: default;
}

/* Dropping a photo onto the conversation. */
.wa__main--drop .wa__msgs {
  outline: 3px dashed var(--wa-accent);
  outline-offset: -10px;
}

/* ---- the full-size photo ------------------------------------------------ */

.wa-viewer {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 40px;
  background: rgba(11, 20, 26, .92);
}

.wa-viewer[data-open='true'] {
  display: flex;
}

.wa-viewer img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 4px;
}

.wa-viewer__close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, .12);
  color: #ffffff;
  cursor: pointer;
}

.wa-viewer__close svg {
  width: 22px;
  height: 22px;
}

/* ---- phone: one pane at a time ----------------------------------------- */

@media (max-width: 700px) {
  /* In the phone menu an invisible field would leave a hole; take it out. */
  body[data-view='chat'] #searchForm {
    display: none;
  }

  #chatView.view--active {
    min-height: calc(100dvh - 140px);
    padding-inline: 0;
    padding-bottom: 0;
  }

  .wa {
    grid-template-columns: minmax(0, 1fr);
    border-radius: 0;
    border-inline: none;
  }

  .wa[data-pane='list'] .wa__main,
  .wa[data-pane='conv'] .wa__side {
    display: none;
  }

  .wa__back {
    display: grid;
    margin-left: -8px;
  }

  .wa__head {
    gap: 8px;
    padding-left: 8px;
  }

  .wa__msgs {
    padding-inline: 12px;
  }

  .wa__msg {
    max-width: 85%;
  }

  .wa__compose {
    padding: 8px;
  }
}
