/*
 * Sanrad Payments -- mobile-first stylesheet.
 *
 * Built for one-handed phone use first: a fixed bottom nav within thumb
 * reach, 16px minimum input text (anything smaller makes iOS Safari zoom on
 * focus), and 44px minimum tap targets. The desktop layout is a widening of
 * the same structure, not a separate design.
 *
 * Brand blue #5b9bd5 is sampled from the supplied logo.
 *
 * THEMING has three states, not two. An explicit choice stamps
 * data-theme="light|dark" on <html>, server-side from the `theme` cookie; the
 * default follows the browser and stamps nothing, so only prefers-color-scheme
 * separates light from dark there. Every colour is a token defined on bare
 * :root, redefined in the media query (guarded so an explicit light choice
 * beats a dark OS) and again under [data-theme="dark"] so the toggle wins in
 * both directions.
 *
 * Never write a colour whose only definition lives inside a media or
 * [data-theme] block -- it would not apply in the unstamped state. And never
 * write a bare hex outside these blocks: it cannot follow the theme, which is
 * how a white panel survives into dark mode.
 *
 * Palette carried over from the Sanrad Hotspot Manager, so the two portals
 * read as the same system.
 */

:root {
  --brand: #5b9bd5;
  /* Two jobs, one value: the hover fill under .btn, and text sitting on a
     tint (pills, alerts, chips). Both want "further from the background than
     --brand" -- which is darker in light mode and brighter in dark. */
  --brand-dark: #3d7cb0;
  --brand-tint: #eaf3fb;
  /* Text on a filled accent -- brand and danger both. White reads on a
     saturated light-mode fill; in dark mode the fills become pastel and this
     flips to near-black, which is why it is a token and not a literal. */
  --brand-ink: #ffffff;

  --ink: #16202b;
  --ink-soft: #5a6b7c;
  --line: #dfe6ee;
  --surface: #ffffff;
  --canvas: #f4f7fa;

  --ok: #1f8a54;
  --ok-tint: #e6f4ec;
  --ok-edge: #bfe3ce;
  --warn: #a9700a;
  --warn-tint: #fdf3e0;
  --warn-edge: #f0dcb4;
  --danger: #c23b3b;
  --danger-tint: #fbeaea;
  --danger-edge: #f0c5c5;
  --danger-hover: #a33131;
  --info-tint: #eaf3fb;
  --info-edge: #c9e0f2;

  --focus-ring: rgba(91, 155, 213, .22);
  /* The dim behind a dialog, and the glass panel over it. The panel's
     background is opaque enough to read on its own: backdrop-filter is an
     enhancement, not structure. */
  --scrim: rgba(22, 32, 43, .34);
  --glass: rgba(255, 255, 255, .82);
  --glass-edge: rgba(255, 255, 255, .65);
  /* The voucher overlay is a large panel, so it is thinner and more strongly
     blurred than the little confirm dialog -- at .82 a sheet that size reads
     as a plain card and the effect is lost. Its own tokens rather than
     retuning --glass, which would change every dialog in the portal. */
  --vm-glass: rgba(255, 255, 255, .58);
  --vm-glass-2: rgba(255, 255, 255, .30);
  --vm-glass-edge: rgba(255, 255, 255, .80);
  --vm-blur: 26px;
  --vm-scrim: rgba(64, 96, 128, .42);

  --radius: 10px;
  --radius-sm: 7px;
  --shadow: 0 1px 2px rgba(22, 32, 43, .06), 0 4px 12px rgba(22, 32, 43, .05);
  --shadow-lg: 0 8px 30px rgba(22, 32, 43, .18);
  --nav-h: 60px;
  /* Measured, not guessed: the line wraps to two rows at phone widths and
     needs 30px, so 40px leaves room for a taller font fallback without
     reserving a band of empty screen above the safe area. */
  --footer-h: 40px;
  /* Everything .page must scroll clear of. The safe-area inset is part of it:
     .app-dock adds that inset to its own height, so reserving only
     --nav-h + --footer-h leaves content under the bar on notched phones. */
  --dock-h: calc(var(--nav-h) + var(--footer-h) + env(safe-area-inset-bottom));
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* Following the browser, when no explicit choice has been stamped. The
   :not([data-theme="light"]) guard is what lets somebody on a dark OS pin the
   portal to light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --brand: #7db4e0;
    --brand-dark: #a3cbec;
    --brand-tint: #14293a;
    --brand-ink: #0d141b;

    --ink: #e6edf5;
    --ink-soft: #a2b2c2;
    --line: #27333e;
    --surface: #151d26;
    --canvas: #0e141a;

    --ok: #4cbb84;      --ok-tint: #10251a;     --ok-edge: #1f4a34;
    --warn: #d9a63f;    --warn-tint: #2a2012;   --warn-edge: #55411c;
    --danger: #e08585;  --danger-tint: #2b1717; --danger-edge: #5a2c2c;
  --danger-hover: #eda6a6;
    --danger-hover: #eda6a6;
    --info-tint: #14293a;                       --info-edge: #2b4256;

    --focus-ring: rgba(125, 180, 224, .30);
    --scrim: rgba(2, 5, 9, .72);
    --glass: rgba(24, 33, 43, .84);
    --glass-edge: rgba(255, 255, 255, .10);
    --vm-glass: rgba(21, 29, 38, .62);
    --vm-glass-2: rgba(46, 62, 80, .34);
    --vm-glass-edge: rgba(255, 255, 255, .16);
    --vm-blur: 26px;
    --vm-scrim: rgba(2, 6, 12, .66);

    --shadow: 0 1px 2px rgba(0, 0, 0, .40), 0 4px 14px rgba(0, 0, 0, .32);
    --shadow-lg: 0 10px 34px rgba(0, 0, 0, .50);
  }
}

/* An explicit dark choice, which must win on a light OS too -- hence a second
   copy rather than only the media query above. */
:root[data-theme="dark"] {
  --brand: #7db4e0;
  --brand-dark: #a3cbec;
  --brand-tint: #14293a;
  --brand-ink: #0d141b;

  --ink: #e6edf5;
  --ink-soft: #a2b2c2;
  --line: #27333e;
  --surface: #151d26;
  --canvas: #0e141a;

  --ok: #4cbb84;      --ok-tint: #10251a;     --ok-edge: #1f4a34;
  --warn: #d9a63f;    --warn-tint: #2a2012;   --warn-edge: #55411c;
  --danger: #e08585;  --danger-tint: #2b1717; --danger-edge: #5a2c2c;
  --danger-hover: #eda6a6;
  --info-tint: #14293a;                       --info-edge: #2b4256;

  --focus-ring: rgba(125, 180, 224, .30);
  --scrim: rgba(2, 5, 9, .72);
  --glass: rgba(24, 33, 43, .84);
  --glass-edge: rgba(255, 255, 255, .10);
  --vm-glass: rgba(21, 29, 38, .62);
  --vm-glass-2: rgba(46, 62, 80, .34);
  --vm-glass-edge: rgba(255, 255, 255, .16);
  --vm-blur: 26px;
  --vm-scrim: rgba(2, 6, 12, .66);

  --shadow: 0 1px 2px rgba(0, 0, 0, .40), 0 4px 14px rgba(0, 0, 0, .32);
  --shadow-lg: 0 10px 34px rgba(0, 0, 0, .50);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--canvas);
  -webkit-text-size-adjust: 100%;
}

/* ---------- Layout ---------- */
.app-header {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.app-header img { height: 28px; width: auto; }
.app-header .spacer { margin-left: auto; }

.page {
  max-width: 760px;
  margin: 0 auto;
  padding: 16px 16px calc(var(--dock-h) + 28px);
}
/* The dock is fixed at every width, so the clearance below must not be
   dropped on desktop -- doing that was what put the last row of buttons
   underneath the bar on wide screens. Only the measure changes here. */
@media (min-width: 900px) {
  .page { max-width: 1040px; }
  /* One line at desktop width (16px), so the strip can be shallower. */
  :root { --footer-h: 28px; }
}
/* Wider screens get wider pages. The log and queue tables have eight or nine
   columns, and squeezing them into 1040px was what forced the sideways
   scrolling -- there was plenty of room either side of it. */
@media (min-width: 1280px) { .page { max-width: 1240px; } }
@media (min-width: 1600px) { .page { max-width: 1480px; } }

h1 { font-size: 1.35rem; margin: 4px 0 16px; letter-spacing: -.01em; }
h2 { font-size: 1.05rem; margin: 24px 0 10px; }
.muted { color: var(--ink-soft); }
.small { font-size: .85rem; }

/* ---------- Cards ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: var(--shadow);
}
.card-tight { padding: 12px 14px; }
.card-link { display: block; color: inherit; text-decoration: none; }
.card-link:active { background: var(--brand-tint); }

.row { display: flex; align-items: center; gap: 12px; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.stack > * + * { margin-top: 10px; }

/* Amounts are tabular so columns of money line up. */
.amount {
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  letter-spacing: -.01em;
}
.amount-lg { font-size: 1.5rem; }

/* ---------- Forms ---------- */
label { display: block; font-size: .875rem; font-weight: 600; margin-bottom: 5px; }
.hint { font-size: .8rem; color: var(--ink-soft); margin-top: 4px; font-weight: 400; }

input[type=text], input[type=password], input[type=email], input[type=tel],
input[type=number], input[type=date], input[type=datetime-local],
input[type=file], select, textarea {
  width: 100%;
  /* 16px exactly -- smaller triggers iOS Safari's zoom-on-focus. */
  font-size: 16px;
  font-family: inherit;
  padding: 11px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  min-height: 44px;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--focus-ring);
}
.field { margin-bottom: 14px; }
.field-error input, .field-error select { border-color: var(--danger); }
.error-text { color: var(--danger); font-size: .82rem; margin-top: 4px; }

.otp-input {
  font-size: 1.6rem !important;
  letter-spacing: .5em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: 46px; padding: 11px 18px;
  font-size: 1rem; font-weight: 600; font-family: inherit;
  border-radius: var(--radius-sm); border: 1px solid transparent;
  background: var(--brand); color: var(--brand-ink);
  cursor: pointer; text-decoration: none;
}
.btn:hover { background: var(--brand-dark); }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn-block { width: 100%; }
.btn-secondary { background: var(--surface); color: var(--ink); border-color: var(--line); }
.btn-secondary:hover { background: var(--canvas); }
.btn-danger { background: var(--danger); color: var(--brand-ink); }
.btn-danger:hover { background: var(--danger-hover); }
.btn-sm { min-height: 38px; padding: 7px 12px; font-size: .875rem; }

/* ---------- Status pills ---------- */
.pill {
  display: inline-block; padding: 3px 10px; border-radius: 999px;
  font-size: .75rem; font-weight: 700; letter-spacing: .02em;
  text-transform: uppercase; white-space: nowrap;
}
.pill-requested  { background: var(--info-tint);  color: var(--brand-dark); }
.pill-processing { background: var(--warn-tint);  color: var(--warn); }
.pill-approved   { background: var(--info-tint);  color: var(--brand-dark); }
.pill-processed  { background: var(--ok-tint);    color: var(--ok); }
.pill-complete   { background: var(--ok-tint);    color: var(--ok); }
.pill-rejected   { background: var(--danger-tint);color: var(--danger); }

/* ---------- Messages ---------- */
.alert {
  padding: 11px 14px; border-radius: var(--radius-sm);
  margin-bottom: 12px; font-size: .9rem; border: 1px solid transparent;
}
.alert-success { background: var(--ok-tint); color: var(--ok); border-color: var(--ok-edge); }
.alert-error   { background: var(--danger-tint); color: var(--danger); border-color: var(--danger-edge); }
.alert-warning { background: var(--warn-tint); color: var(--warn); border-color: var(--warn-edge); }
.alert-info    { background: var(--info-tint); color: var(--brand-dark); border-color: var(--info-edge); }

/* ---------- Bottom navigation ---------- */
/* The pinned stack at the foot of the screen: nav bar, copyright strip under
   it. One fixed element rather than two, so they cannot drift apart and so
   .page has a single height to clear -- see --dock-h. */
.app-dock {
  position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
  background: var(--surface);
  border-top: 1px solid var(--line);
  /* Clears the iOS home indicator. */
  padding-bottom: env(safe-area-inset-bottom);
}

.bottom-nav {
  display: flex;
}

.app-footer {
  height: var(--footer-h);
  display: flex; align-items: center; justify-content: center;
  padding: 0 12px; text-align: center;
  border-top: 1px solid var(--line);
  color: var(--ink-soft); font-size: .68rem; line-height: 1.3;
}
.bottom-nav a {
  flex: 1; min-height: var(--nav-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; text-decoration: none;
  color: var(--ink-soft); font-size: .68rem; font-weight: 600;
}
.bottom-nav a.active { color: var(--brand); }
.bottom-nav svg { width: 21px; height: 21px; }

.badge {
  position: absolute; transform: translate(12px, -10px);
  background: var(--danger); color: var(--brand-ink);
  font-size: .62rem; font-weight: 700;
  min-width: 17px; height: 17px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center; padding: 0 4px;
}

/* A field whose value is settled by a rule rather than typed -- an LEI code
   filled in from an exemption, or fixed because the beneficiary is staff.
   Still submitted, unlike a disabled input, so the value reaches the server. */
.input-locked {
  background: var(--canvas);
  color: var(--ink-soft);
  cursor: not-allowed;
}

/* My / All toggle above a list. A segmented control rather than two buttons,
   so it reads as one choice with a current state. */
.scope-switch {
  display: flex;
  margin-bottom: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface);
}
.scope-switch a {
  flex: 1;
  padding: 9px 12px;
  text-align: center;
  font-size: .875rem;
  font-weight: 600;
  text-decoration: none;
  color: var(--ink-soft);
}
.scope-switch a + a { border-left: 1px solid var(--line); }
.scope-switch a.active { background: var(--brand); color: var(--brand-ink); }

/* Bulk selection: a desktop-only affordance. Hidden on phones rather than
   restyled, so the mobile-first layout is never compromised for a job that is
   done sitting at a machine. */
.bulk-only { display: none; }
.bulk-row { display: block; }
.bulk-row .list-item { display: block; }

@media (min-width: 900px) {
  .bulk-only { display: inline-flex; }
  .bulk-row {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: 10px;
  }
  .bulk-check { width: auto; min-height: 0; }
  /* Sits above the list, under the filter, and stays put while the rows scroll
     past -- the button acts on the selection, so it should not be somewhere you
     have to scroll fifty rows to find. */
  .bulk-bar {
    position: sticky; top: 12px; z-index: 5;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 10px 14px; margin-bottom: 12px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
  }
  .bulk-all { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
  .bulk-all input { width: auto; min-height: 0; margin: 0; }
  .bulk-table { width: 100%; border-collapse: collapse; }
  .bulk-table th, .bulk-table td {
    text-align: left; padding: 10px 12px; vertical-align: top;
    border-bottom: 1px solid var(--line);
  }
  .bulk-table th { font-size: .8rem; color: var(--ink-soft); }
  .bulk-table .field { margin-bottom: 8px; }
}

/* Filters sitting under each column heading, in place of one search box.
   No apply button: selects submit on change, text boxes on Enter via a
   visually hidden submit that exists only so the browser does that. */
.filter-row th { padding-top: 0; }
.filter-row input, .filter-row select {
  width: 100%;
  min-height: 34px;
  padding: 5px 8px;
  font-size: .8rem;
  font-weight: 400;
}
.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;
}
.nowrap { white-space: nowrap; }
/* A user agent string is long and not worth a column's width; the full value
   is on the title attribute for when it matters. */
.agent-cell {
  max-width: 220px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}

/* Same rows, two shapes: a table where there is width for one, cards where
   there is not. */
.desktop-only { display: none; }
.mobile-only { display: block; }
@media (min-width: 900px) {
  .desktop-only { display: block; }
  .mobile-only { display: none; }
}

/* Page heading with its own action on the right -- a back button, or
   anything else that belongs to the page rather than to a card within it.
   The h1's own margin moves onto the row so the two stay aligned. */
.page-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 4px 0 16px;
}
.page-head h1 { margin: 0; }
.page-head .btn { flex-shrink: 0; }

/* Queue preview: an overlay anchored to the control that opened it, scaling
   out of that corner so it reads as belonging to the row rather than to the
   page. The cell is the positioning context, which is why it is relative. */
.preview-cell { position: relative; }

.preview-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  background: var(--surface); color: var(--ink-soft);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  cursor: pointer;
}
.preview-btn:hover { color: var(--brand-dark); border-color: var(--brand); }
.preview-btn svg { width: 18px; height: 18px; }

.preview-panel {
  /* Fixed, not absolute: an absolute panel is clipped by .table-wrap and grows
     the container, which is what was causing the stray vertical scrolling.
     Placement comes from js/popover.js. */
  position: fixed; z-index: 60;
  width: min(360px, calc(100vw - 24px));
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  transform-origin: top right;
  animation: preview-in .12s ease-out;
}
@keyframes preview-in {
  from { opacity: 0; transform: scale(.85); }
  to   { opacity: 1; transform: scale(1); }
}
.preview-pre {
  margin: 4px 0 0;
  max-height: 180px; overflow: auto;
  background: var(--canvas); padding: 8px;
  border-radius: 6px;
  white-space: pre-wrap; word-break: break-word;
}
/* The panel itself is bounded too -- a long provider response must not push it
   off the screen. */
.preview-panel { max-height: 70vh; overflow-y: auto; }

/* Two columns where there is width for them: the message on the left, the
   delivery facts on the right, so neither pushes the other down the panel. */
.preview-cols { display: grid; gap: 12px; }
/* The delivery column is a flex column so its footnote can be pushed to the
   bottom with margin-top:auto, sitting level with the foot of the panel rather
   than floating directly under whatever happens to precede it. */
.preview-col { display: flex; flex-direction: column; }
.preview-note { margin-top: auto; padding-top: 10px; }
@media (min-width: 900px) {
  .preview-panel { width: min(680px, calc(100vw - 24px)); }
  .preview-cols { grid-template-columns: 1fr 1fr; }
  .preview-cols > div { min-width: 0; }
}

.preview-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 8px;
}
.preview-close {
  background: none; border: 0; cursor: pointer;
  font-size: 1.2rem; line-height: 1; color: var(--ink-soft);
  padding: 0 2px;
}
.preview-close:hover { color: var(--danger); }

/* The panel is fixed now, so the phone case needs no special anchoring --
   popover.js keeps it inside the viewport at any width. */

/* Terminal scan target. Large and central: the person is standing at a
   machine, not reading a form. */
.scan-target {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 28px 16px; margin: 8px 0 4px;
  border: 2px dashed var(--line); border-radius: var(--radius);
  color: var(--ink-soft); text-align: center;
}
.scan-target svg { width: 56px; height: 56px; }
.scan-target.scanning { border-color: var(--brand); color: var(--brand-dark); }

/* ---------- Lists & tables ---------- */
.list-item {
  display: block; padding: 13px 0; border-bottom: 1px solid var(--line);
  text-decoration: none; color: inherit;
}
.list-item:last-child { border-bottom: 0; }

/* A payment row plus its document buttons. The buttons sit beside the link
   rather than inside it -- see payments/_row.html -- so the border that used
   to belong to .list-item moves out here whenever a row carries actions, or
   the rule would cut between the row and its own buttons. */
.list-row { border-bottom: 1px solid var(--line); }
.list-row .list-item { border-bottom: 0; }
/* Only the final row in a card loses its rule. Scoped rather than written as
   .list-row:last-child, because in a bulk list the row is wrapped in
   .bulk-row and is therefore always its parent's last child -- which would
   have removed every separator on the page. */
.card > .list-row:last-child,
.card > .bulk-row:last-child > .list-row { border-bottom: 0; }
.list-row-actions {
  display: flex; gap: 6px; flex-wrap: wrap;
  padding-bottom: 12px; margin-top: -4px;
}
.list-row-actions .btn { flex: 0 0 auto; }
@media (max-width: 420px) {
  /* Thumb reach: on a phone the two buttons share the width rather than
     leaving a stray tap target hugging the left edge. */
  .list-row-actions .btn { flex: 1 1 0; }
}

/* Pages that are mostly a wide table opt out of the reading measure. 1040px
   is right for a column of cards and prose; the payment report has ten
   columns, and holding it to that measure meant scrolling sideways through a
   table with empty margins either side of it. Opt-in via {% block page_class %}
   rather than applied to .page, because the measure is correct everywhere
   else -- a form or a list of cards does not get better at 1800px.

   Still capped rather than edge-to-edge: a table spanning a 34" display is
   unreadable for the opposite reason, the eye losing the row on the way
   across. */
@media (min-width: 1280px) { .page-wide { max-width: min(96vw, 1600px); } }
@media (min-width: 1800px) { .page-wide { max-width: min(96vw, 1860px); } }

/* Status breakdown as a row of tiles rather than a six-row table. Six numbers
   read as a row and not as a list, the table shape wasted most of its width on
   three empty columns, and auto-fit means the row reflows to two or three
   across on a phone without a breakpoint for each. */
.stat-row {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.stat-tile {
  /* --canvas, the page ground: a tile inset into a card reads as recessed
     rather than as a card stacked on a card. It follows the theme like every
     other token, which a hex here would not. */
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
}
/* A status that did not occur is kept, not dropped -- an absent row reads as
   "no such stage" rather than "none at this stage" -- but it recedes so the
   eye lands on the stages that have something in them. */
.stat-tile.is-empty { opacity: .55; }
.stat-tile .stat-value {
  display: block; margin-top: 8px;
  font-size: 1.25rem; font-weight: 650; font-variant-numeric: tabular-nums;
}
.stat-tile .stat-sub {
  display: block; margin-top: 2px;
  font-size: .8rem; color: var(--ink-soft); font-variant-numeric: tabular-nums;
}

/* The report's filter grid. Auto-fit rather than a fixed column count: the
   eight filters sit one per row on a phone and flow into three or four across
   on a desktop without a breakpoint per width. */
.report-filters {
  display: grid; gap: 0 12px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}

/* A heading belonging to a card rather than to the page. The top margin goes
   because the card already provides that space. */
.card-title { margin: 0 0 10px; font-size: 1rem; }

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line); }
th { font-size: .75rem; text-transform: uppercase; color: var(--ink-soft); letter-spacing: .03em; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---------- Misc ---------- */
.empty { text-align: center; padding: 40px 20px; color: var(--ink-soft); }
.divider { height: 1px; background: var(--line); margin: 18px 0; }
.kv { display: flex; justify-content: space-between; gap: 16px; padding: 7px 0; font-size: .92rem; }
.kv dt { color: var(--ink-soft); }
.kv dd { margin: 0; text-align: right; font-weight: 550; }

.auth-shell {
  min-height: 100dvh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: 24px 16px;
}
.auth-card { width: 100%; max-width: 400px; }
.auth-footer {
  width: 100%; max-width: 400px; margin-top: 18px;
  text-align: center; color: var(--ink-soft);
  font-size: .68rem; line-height: 1.4;
}
.auth-logo { display: block; margin: 0 auto 22px; height: 34px; }

/* QR scanner viewport */
#qr-reader { width: 100%; border-radius: var(--radius-sm); overflow: hidden; }
#qr-reader video { width: 100% !important; height: auto !important; display: block; }

.pagination { display: flex; gap: 8px; justify-content: center; align-items: center; margin-top: 18px; }
/* Previous/Next are buttons now, so only the page counter needs styling
   here -- leaving the old anchor rules would override .btn's padding. */
.pagination span { padding: 8px 12px; border-radius: var(--radius-sm); font-size: .875rem; }
.pagination .current { background: var(--brand); color: var(--brand-ink); font-weight: 600; }

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

/* ---------- Template variable palette ---------- */
/* Tap targets stay 34px+ so the palette is usable on a phone, where a
   sysadmin may well be configuring templates. */
.var-palette {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px;
  background: var(--canvas);
  margin: -6px 0 14px;
}
.var-preview { display: flex; flex-wrap: wrap; gap: 6px; min-height: 28px; align-items: center; }
.var-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px 6px 4px 11px; font-size: .82rem;
  font-variant-numeric: tabular-nums;
}
.var-chip strong { color: var(--brand-dark); font-weight: 700; }
.var-remove {
  border: 0; background: var(--line); color: var(--ink-soft);
  width: 20px; height: 20px; min-height: 20px; border-radius: 999px;
  font-size: .72rem; line-height: 1; cursor: pointer; padding: 0;
}
.var-remove:hover { background: var(--danger); color: var(--brand-ink); }

.var-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
.var-btn {
  min-height: 34px; padding: 6px 11px;
  font-family: inherit; font-size: .82rem; font-weight: 600;
  background: var(--surface); color: var(--brand-dark);
  border: 1px solid var(--info-edge); border-radius: var(--radius-sm); cursor: pointer;
}
.var-btn:hover { background: var(--brand-tint); border-color: var(--brand); }
/* Valid but rarely right for this event -- de-emphasised, not hidden. */
.var-btn-dim { opacity: .5; }
.var-tag {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .04em;
  background: var(--warn-tint); color: var(--warn);
  padding: 1px 5px; border-radius: 4px; margin-left: 5px; font-weight: 700;
}

/* ---------- Voucher overlay ---------- */
/* The document on the left, the beneficiary's earlier signatures on the right,
   and a verdict at the bottom. Every colour is a token, so it follows the
   theme -- the supplied sketch shows a light and a dark version and they are
   the same layout, not two designs. */
.voucher-modal {
  position: fixed; inset: 0; z-index: 220;
  display: none; align-items: center; justify-content: center;
  padding: 18px;
  background: var(--vm-scrim);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.voucher-modal.open { display: flex; }
/* The page behind must not scroll under the overlay on a phone. */
body.vm-locked { overflow: hidden; }

.vm-shell {
  width: 100%; max-width: 1140px; max-height: 100%;
  display: flex; flex-direction: column;
  /* Frosted glass: a translucent, blurred sheet with a lit top-left edge, the
     page showing through behind it. The gradient is what stops it looking
     like flat transparency -- real glass catches light unevenly.
     backdrop-filter is an enhancement: where it is unsupported the gradient
     alone still gives an opaque-enough panel to read against. */
  background:
    linear-gradient(135deg, var(--vm-glass) 0%, var(--vm-glass-2) 100%);
  -webkit-backdrop-filter: blur(var(--vm-blur)) saturate(165%);
  backdrop-filter: blur(var(--vm-blur)) saturate(165%);
  border: 1px solid var(--vm-glass-edge);
  border-radius: 16px;
  box-shadow:
    var(--shadow-lg),
    inset 0 1px 0 var(--vm-glass-edge);
  overflow: hidden;
}
/* Where the blur cannot be done, fall back to the solid surface rather than
   leaving text over a see-through sheet. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .vm-shell { background: var(--surface); }
}
.vm-head {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--vm-glass-edge);
}
.vm-head h2 {
  margin: 0; font-size: 1.02rem; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vm-close {
  margin-left: auto; flex: 0 0 auto;
  width: 40px; height: 40px; border-radius: 50%;
  border: 0; cursor: pointer;
  background: var(--danger); color: var(--brand-ink);
  font: inherit; font-weight: 700; line-height: 1;
}
.vm-close:hover { background: var(--danger-hover); }

.vm-body { display: flex; gap: 14px; padding: 14px; overflow: auto; }
.vm-doc { flex: 1 1 62%; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.vm-doc iframe {
  width: 100%; height: 62vh; min-height: 320px;
  border: 1px solid var(--vm-glass-edge); border-radius: var(--radius-sm);
  background: #fff;  /* a PDF is a white page; it is not part of the theme */
}
.vm-fallback { align-self: flex-start; }

.vm-side { flex: 1 1 38%; min-width: 250px; display: flex; flex-direction: column; gap: 12px; }
/* A plain document -- the layout sample sheets -- has no comparison panel, so
   the sheet takes the whole width instead of sitting beside an empty column.
   Taller too: there is nothing else competing for the height. */
.vm-doc-only .vm-side { display: none; }
.vm-doc-only .vm-doc iframe { height: 74vh; }
.vm-samples h3 {
  margin: 0 0 10px; font-size: .92rem; font-weight: 600;
}
.vm-dim { color: var(--ink-soft); font-weight: 400; }
.vm-sample { margin: 0 0 12px; }
.vm-sample img {
  display: block; width: 100%; height: 92px; object-fit: contain;
  background: #fff;  /* signatures are drawn in dark ink on transparency */
  border: 1px solid var(--vm-glass-edge); border-radius: var(--radius-sm);
  padding: 4px;
}
.vm-sample figcaption { margin-top: 5px; font-size: .78rem; color: var(--ink-soft); }
.vm-empty { font-size: .85rem; color: var(--ink-soft); margin: 0; }

.vm-flagged {
  font-size: .82rem; padding: 10px 12px;
  background: var(--warn-tint); color: var(--warn);
  border: 1px solid var(--warn-edge); border-radius: var(--radius-sm);
}

.vm-verdict { margin-top: auto; }
.vm-note-label { display: block; font-size: .8rem; color: var(--ink-soft); margin-bottom: 4px; }
.vm-verdict textarea { width: 100%; margin-bottom: 10px; }
.vm-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.vm-actions .btn { flex: 1 1 auto; }
/* Green, per the sketch -- and it is the only green button in the portal,
   which is the point: it is the "nothing to do here" answer. */
.vm-ok { background: var(--ok); color: var(--brand-ink); }
.vm-ok:hover { background: var(--ok); filter: brightness(1.08); }
.vm-clear { margin-top: 8px; width: 100%; }

/* Phones: the document and the samples stack, document first. Comparing two
   signatures side by side needs width that is not there, so the panel goes
   under it rather than being squeezed into a column too narrow to judge. */
@media (max-width: 780px) {
  .voucher-modal { padding: 0; }
  .vm-shell { max-width: none; height: 100%; border-radius: 0; border: 0; }
  /* Edge to edge on a phone there is no glass edge to catch light. */
  .vm-body { flex-direction: column; }
  .vm-doc iframe { height: 46vh; }
  .vm-side { min-width: 0; }
}

/* ---------- Header icon buttons ---------- */
/* Admin and Sign out. Round, coloured, no label -- the words live in
   aria-label and title. Slightly larger than the theme control beside them
   because these are real actions rather than a preference, and one of them
   ends the session. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: 38px; height: 38px; border-radius: 50%;
  border: 0; cursor: pointer; text-decoration: none;
}
.icon-btn svg { width: 19px; height: 19px; display: block; }
.icon-btn-brand { background: var(--brand); color: var(--brand-ink); }
.icon-btn-brand:hover { background: var(--brand-dark); }
.icon-btn-danger { background: var(--danger); color: var(--brand-ink); }
.icon-btn-danger:hover { background: var(--danger-hover); }
/* An icon that sits beside a real button without competing with it. */
.icon-btn-ghost {
  background: var(--surface); color: var(--ink-soft);
  border: 1px solid var(--line);
}
.icon-btn-ghost:hover { color: var(--brand); border-color: var(--brand); }

/* Actions grouped at one end of a .row-between, so a card does not change
   shape depending on how many of them the viewer is allowed to see. */
.card-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
@media (max-width: 460px) {
  .icon-btn { width: 36px; height: 36px; }
  .icon-btn svg { width: 18px; height: 18px; }
}

/* ---------- Theme toggle ---------- */
/* A three-way segmented control in the header: Auto follows the browser,
   Light and Dark pin it. Real submit buttons in a real form -- the choice is
   stored server-side in a cookie and stamped on <html> before the page is
   sent, so there is no flash of the wrong theme on load and it works with
   JavaScript off. */
.theme-toggle {
  --tt-size: 32px; --tt-gap: 3px; --tt-pos: 0;
  position: relative;
  display: flex; gap: var(--tt-gap); padding: 2px;
  border: 1px solid var(--line); border-radius: 20px;
}
.theme-toggle button {
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; color: var(--ink-soft); cursor: pointer;
  padding: 0; border-radius: 50%;
  /* Below the 44px floor the rest of the portal keeps, deliberately: this is a
     rarely-used preference sitting in a 56px header beside two real actions,
     and at full height it would crowd them off a narrow phone. Square, so the
     lit state is a disc rather than a lozenge. */
  width: var(--tt-size); height: var(--tt-size); min-height: 0;
  position: relative; z-index: 1;   /* above the sliding thumb */
}
.theme-toggle svg { width: 17px; height: 17px; display: block; }
/* The lit disc is one element that slides to whichever button is on, rather
   than a background that jumps between them. It has its own view-transition
   name, so during the page cross-fade the browser moves it from the old
   position to the new instead of fading it along with everything else.
   Placed by :has(); a browser without it keeps the plain lit button below. */
.theme-thumb {
  position: absolute; top: 2px; left: 2px;
  width: var(--tt-size); height: var(--tt-size); border-radius: 50%;
  background: var(--brand);
  transform: translateX(calc(var(--tt-pos) * (var(--tt-size) + var(--tt-gap))));
  /* No transition of its own: the view transition slides it, and where there
     is none, .theme-switching below does. Both at once would photograph it
     barely moved and then slide it again underneath. */
  view-transition-name: theme-thumb;
}
.theme-toggle:has(button.on[value="auto"]) { --tt-pos: 1; }
.theme-toggle:has(button.on[value="dark"]) { --tt-pos: 2; }
@supports not selector(:has(a)) {
  .theme-thumb { display: none; }
  .theme-toggle button.on { background: var(--brand); }
}
/* A theme switch eases the colours rather than snapping, where the browser has
   no view transition to cross-fade with (static/js/theme_toggle.js). Only
   while the class is on <html>, so no other change picks it up. */
@media (prefers-reduced-motion: no-preference) {
  .theme-switching, .theme-switching *, .theme-switching *::before, .theme-switching *::after {
    transition: background-color .25s ease, color .25s ease, border-color .25s ease,
                fill .25s ease, stroke .25s ease, transform .25s ease !important;
  }
  ::view-transition-old(root), ::view-transition-new(root) { animation-duration: .25s; }
  ::view-transition-group(theme-thumb) { animation-duration: .25s; animation-timing-function: ease; }
}
.theme-toggle button:hover { color: var(--ink); }
.theme-toggle button.on { color: var(--brand-ink); }

/* Narrow phones: the header carries a 140px wordmark, this control, Admin and
   Sign out. Symbols already fit where three words did not, so this only
   tightens the surroundings. */
@media (max-width: 460px) {
  .app-header { gap: 8px; padding: 10px 12px; }
  .app-header img { height: 24px; }
  .theme-toggle { --tt-size: 30px; }
}

/* ---------- WhatsApp message preview ---------- */
/* Deliberately imitates WhatsApp's own bubble so an admin can judge the
   wording as a recipient would see it, not as a form field.
   
   The one block in this file that does NOT follow the portal theme, and that
   is the point: it is a picture of another application, so it keeps WhatsApp's
   own colours whichever theme the portal is in. Every colour inside it --
   background and text both -- is therefore stated outright, so it stays
   legible sitting on a dark page. */
.wa-preview {
  background: #e6ddd4;
  background-image: radial-gradient(rgba(255,255,255,.35) 1px, transparent 1px);
  background-size: 14px 14px;
  padding: 16px 14px;
  border-radius: var(--radius);
  max-width: 380px;
}
.wa-bubble {
  position: relative;
  background: #fff;
  border-radius: 8px 8px 8px 2px;
  padding: 8px 10px 6px;
  box-shadow: 0 1px 1px rgba(0,0,0,.13);
  font-size: .93rem;
  line-height: 1.45;
  color: #111b21;
  word-break: break-word;
}
.wa-header { font-weight: 700; margin-bottom: 4px; }
.wa-body { white-space: pre-wrap; }
.wa-footer { margin-top: 6px; font-size: .78rem; color: #667781; }
.wa-time { text-align: right; font-size: .68rem; color: #667781; margin-top: 2px; }
.wa-button {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  background: #fff; color: #0a7cff;
  margin-top: 2px; padding: 9px; border-radius: 2px 2px 8px 8px;
  font-size: .9rem; font-weight: 500;
  box-shadow: 0 1px 1px rgba(0,0,0,.13);
  border-top: 1px solid #e9edef;
}
.wa-button + .wa-button { border-radius: 2px; }
.wa-button:last-child { border-radius: 2px 2px 8px 8px; }
.wa-button svg { width: 15px; height: 15px; }

/* Two-column on desktop: form on the left, live preview on the right. */
@media (min-width: 900px) {
  .wa-split { display: grid; grid-template-columns: 1fr 400px; gap: 20px; align-items: start; }
  .wa-split-preview { position: sticky; top: 80px; }
}

.slot-row {
  display: grid; grid-template-columns: 62px 1fr; gap: 10px;
  align-items: center; padding: 9px 0; border-bottom: 1px solid var(--line);
}
.slot-tag {
  font-weight: 700; color: var(--brand-dark); font-variant-numeric: tabular-nums;
  background: var(--brand-tint); padding: 5px 8px; border-radius: 6px;
  text-align: center; font-size: .85rem;
}
.slot-sample { font-size: .78rem; color: var(--ink-soft); margin-top: 3px; }

/* ---------- Avatar ---------- */
/* One image, shown at two sizes by CSS only -- there are no generated
   thumbnail files. object-fit keeps non-square uploads from distorting. */
.avatar {
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface);
  border: 1px solid var(--line);
  flex-shrink: 0;
  display: block;
}
.avatar-sm { width: 46px; height: 46px; }
.avatar-lg { width: 110px; height: 110px; }
.avatar-wrap { display: flex; align-items: center; gap: 13px; }

/* Visually hidden but still focusable and operable by assistive tech.
   Used for the profile photo input, which is driven by a styled <label>:
   the native control is unreadable on a phone and prints the chosen
   filename next to the picture. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}


/* --- Glass confirmation modal -------------------------------------------
   Replaces window.confirm() for destructive actions. The browser dialog could
   not be themed, blocked the page, and read as though it came from somewhere
   other than the portal.

   The glass effect is a progressive enhancement: where backdrop-filter is
   unsupported the panel simply falls back to its solid background, which is
   why the background colour is opaque enough to read against on its own. */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 200;
  display: none; align-items: center; justify-content: center; padding: 20px;
  background: var(--scrim);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
}
.modal-backdrop.open { display: flex; }

.modal-glass {
  width: 100%; max-width: 380px;
  background: var(--glass);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 20px;
}
.modal-glass h2 { margin: 0 0 6px; font-size: 1.05rem; }
.modal-glass p { margin: 0 0 16px; color: var(--ink-soft); font-size: .88rem; }
.modal-actions { display: flex; gap: 8px; }
/* Each scope in the export dialog: a line saying what it is and how much,
   then its two formats. Separated by a rule rather than by space alone --
   four buttons in a column otherwise read as one list of four, when they are
   two answers to two different questions. */
.export-choice { margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.export-choice .small { margin-bottom: 8px; }

/* An icon control that is in its place but has nothing to do -- Clear with no
   filters set. Kept on screen rather than removed: a pane that shows one icon
   sometimes and two at other times reads as one having replaced the other.
   Not pointer-events:none, so the title still explains it on hover. */
.icon-btn.is-inert { opacity: .38; cursor: default; }
.icon-btn.is-inert:hover { color: inherit; border-color: var(--line); }
.modal-glass .export-choice + .export-choice { border-bottom: none; }
.modal-actions .btn { flex: 1; }

@media (prefers-reduced-motion: no-preference) {
  .modal-backdrop.open .modal-glass { animation: modal-in .14s ease-out; }
  @keyframes modal-in {
    from { opacity: 0; transform: translateY(6px) scale(.98); }
    to   { opacity: 1; transform: none; }
  }
}


/* "Working…" -- covers the page from the moment a POST form is submitted until
   the next page arrives; see static/js/busy.js. It takes every click, so a
   second press has nothing to land on. No fade: a transition does not advance
   in a window the browser is not painting, and a fade that never finishes is
   an overlay nobody sees. Deliberately wordless beyond "Working…": it only
   has to say that the first press was heard. */
.busy-overlay {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim);
  cursor: progress;
  /* The second click of a double click lands here; without this it selects
     the word under it. */
  -webkit-user-select: none; user-select: none;
}
.busy-overlay[hidden] { display: none; }
.busy-panel {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 20px;
  background: var(--glass);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  font-weight: 600;
}
.busy-spinner {
  width: 20px; height: 20px; flex: none;
  border: 3px solid var(--line);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: busy-spin .8s linear infinite;
}
@keyframes busy-spin { to { transform: rotate(360deg); } }

/* Flash messages fade out after six seconds -- see static/js/toasts.js.
   Only .toast, never .alert on its own: the same alert styling carries inline
   notices that are part of a page and must stay put. */
.toast { transition: opacity .28s ease, transform .28s ease; }
.toast-gone { opacity: 0; transform: translateY(-4px); }
@media (prefers-reduced-motion: reduce) {
  .toast { transition: none; }
}

/* "or" between the password form and "Sign in with Sanrad Identity". */
.sso-divider {
  display: flex; align-items: center; gap: 10px;
  margin: 16px 0 12px;
}
.sso-divider::before, .sso-divider::after {
  content: ""; flex: 1; border-top: 1px solid var(--line);
}
