/*
 * Sanrad UI — the shared look for every Sanrad web portal.
 *
 * Version 1.4.0 · 2026-09-21 · extracted from Sanrad Payments v2.118
 * (app/static/css/portal.css). Both palettes are Payments' own, live today.
 *
 * Copy this file into your portal and serve it yourself. Do not link to
 * payments.sanrad.net: no portal should stop rendering because another is down.
 * Nothing here loads from anywhere else — no web fonts, no CDN, no analytics.
 *
 * THREE THEMES, SHARED ACROSS PORTALS. Dark, light, and auto (auto follows the
 * browser). The choice lives in one cookie on .sanrad.net, so setting it in any
 * portal sets it in all of them. See README "The shared theme" for the exact
 * cookie, the markup and the anti-flash snippet.
 *
 *   <html data-theme="dark">   pinned dark
 *   <html data-theme="light">  pinned light
 *   <html>                     auto: follows prefers-color-scheme
 *
 * Every colour is defined on bare :root (light), then overridden under
 * prefers-color-scheme for auto, then again under [data-theme="dark"] so an
 * explicit choice beats the browser in both directions. Never write a colour
 * whose only definition lives inside a media block — it would not apply in the
 * unstamped state. And never write a bare hex outside these three blocks: it
 * cannot follow the theme, which is how a white panel survives into dark mode.
 *
 * Contents
 *   1. Tokens          colour (light / auto / dark), type, spacing, radii
 *   2. Base            page, headings, links, scrollbars, focus
 *   3. Layout          header, page, card, page-head, kv list, divider
 *   4. Buttons         primary / secondary / ghost / destructive / icon, groups
 *   5. Forms           label, input, select, textarea, date, checkbox, errors
 *   6. Data            table, pill (status), chip, badge, empty, pagination
 *   7. Feedback        alert, toast, modal, busy overlay, progress
 *   8. Navigation      tabs, segmented control, theme switch, bell, bookmarks
 *   9. Sign-in and public pages
 *  10. Print
 *  11. Capture         signature pad, camera, QR, scan target, photo grid
 *  12. Dense UI        permission grid, masked value, secret, steps, consent,
 *                      filters, message preview, report layout, mobile cards
 */

/* ============================================================ 1. Tokens */
:root {
  color-scheme: light;

  /* -- Accent. --brand-dark is "further from the background than --brand":
     the hover fill, and text on a tint. --brand-ink is text ON a filled
     accent: white on a saturated light fill, near-black on a pastel dark one,
     which is why it is a token and not a literal. */
  --brand: #5b9bd5;
  --brand-dark: #3d7cb0;
  --brand-tint: #eaf3fb;
  --brand-ink: #ffffff;

  /* -- Surfaces and text. --canvas is the page, --surface every panel. */
  --canvas: #f4f7fa;
  --surface: #ffffff;
  --line: #dfe6ee;
  --ink: #16202b;
  --ink-soft: #5a6b7c;
  --ink-faint: #8394a5;

  /* -- Status: text colour, its fill (tint) and its border (edge). */
  --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;

  /* -- Depth. The glass panels are fixed here for every portal: opaque enough
     to read on their own, because backdrop-filter is an enhancement and
     Firefox may or may not apply it. */
  /* --brand-block / --brand-block-ink were retired in 1.2.2. They painted a
     dark tile behind the logo in every theme, on the premise that the wordmark
     is white on transparency. It is not: the supplied artwork is a blue tile
     carrying a white "Sa" plus "nrad®" in blue on transparency -- drawn for a
     LIGHT background. The tile made a near-black pill with a blue box in it.
     If a white-on-transparency export is ever supplied, a dark area may come
     back; until then there is nothing for it to be for. */

  --focus-ring: rgba(91, 155, 213, .22);
  --scrim: rgba(22, 32, 43, .34);
  --glass: rgba(255, 255, 255, .82);
  --glass-edge: rgba(255, 255, 255, .65);
  --glass-blur: 18px;
  --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);

  /* -- Type. 16px base: anything smaller in an input makes iOS Safari zoom. */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  --text-base: 16px;
  --text-sm: .875rem;    /* labels, buttons in tables, tabs */
  --text-xs: .8rem;      /* hints */
  --text-micro: .75rem;  /* uppercase: table headers, pills */
  --line-height: 1.5;
  --weight-normal: 400;
  --weight-medium: 600;
  --weight-strong: 700;

  /* -- Spacing scale: 4 / 8 / 12 / 16 / 24 / 32. Don't invent values between. */
  --space-1: 4px;  --space-2: 8px;  --space-3: 12px;
  --space-4: 16px; --space-5: 24px; --space-6: 32px;

  --radius: 10px;
  --radius-sm: 7px;
  --radius-pill: 999px;

  /* -- Controls. 44px is the touch floor; 46px is the standard button. */
  --control-h: 46px;
  --control-h-sm: 38px;
  --touch-min: 44px;
}

/* Auto: no data-theme stamped, browser says dark. Guarded so a pinned light
   choice still wins. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --brand: #7db4e0;
    --brand-dark: #a3cbec;
    --brand-tint: #14293a;
    --brand-ink: #0d141b;

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

    --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);
    --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);
  }
}

/* Pinned dark: the same values again, so the toggle wins over a light OS. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --brand: #7db4e0;
  --brand-dark: #a3cbec;
  --brand-tint: #14293a;
  --brand-ink: #0d141b;

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

  --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);
  --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);
}

/* ============================================================ 2. Base */
* { box-sizing: border-box; }

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

h1 { font-size: 1.35rem; margin: var(--space-1) 0 var(--space-4); letter-spacing: -.01em; }
h2 { font-size: 1.05rem; margin: var(--space-5) 0 var(--space-3); }
h3 { font-size: .95rem; margin: var(--space-4) 0 var(--space-2); }

a { color: var(--brand); }

.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); }
.small { font-size: .85rem; }
.hint { font-size: var(--text-xs); color: var(--ink-soft); margin-top: var(--space-1); font-weight: var(--weight-normal); }
.error-text { font-size: var(--text-xs); color: var(--danger); margin-top: var(--space-1); }
/* Money and any column of figures: same-width digits so they line up. */
.amount { font-variant-numeric: tabular-nums; font-weight: 650; letter-spacing: -.01em; }
.amount-lg { font-size: 1.5rem; }

/* Scrollbars, or a dark page keeps a light gutter. */
* { scrollbar-color: var(--line) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: var(--radius-pill); }
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }

::selection { background: var(--brand-tint); color: var(--ink); }

:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ============================================================ 3. Layout */
.app-header {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: var(--space-3);
  padding: 10px var(--space-4);
  padding-top: calc(10px + env(safe-area-inset-top, 0px));
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
/* Your logo, sized by the kit as a DEFAULT: :where() gives this the weight of a
   bare `img`, so `.brand-logo-img { height: 28px }` in your own stylesheet wins.
   It used to be `.app-header img` (0,1,1), which beat a portal's one-class rule
   inside the header -- the very place logos live. Same bug as the old
   `.brand img`; found by SLA before it bit anyone twice. */
:where(.app-header) img { height: 28px; width: auto; }
.app-header .spacer { margin-left: auto; }

.page { max-width: 760px; margin: 0 auto; padding: var(--space-4) var(--space-4) var(--space-6); }
.page-wide { max-width: 1100px; }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: var(--space-3);
  box-shadow: var(--shadow);
}
.card-tight { padding: var(--space-3) 14px; }
.card-title { margin: 0 0 10px; font-size: 1rem; }

.page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: var(--space-1) 0 var(--space-4); }
.page-head h1 { margin: 0; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
/* Every action in a .row-between card goes in ONE of these, or space-between
   pushes a middle child to the centre and the card changes shape per viewer. */
.card-actions { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; }

.divider { height: 1px; background: var(--line); margin: 18px 0; }

/* Label/value rows: label left and muted, value right. */
.kv { display: flex; justify-content: space-between; gap: var(--space-4); padding: 7px 0; font-size: .92rem; }
.kv dt { color: var(--ink-soft); }
.kv dd { margin: 0; text-align: right; font-weight: 550; }

.list-item { padding: var(--space-3) 0; border-bottom: 1px solid var(--line); }
.list-item:last-child { border-bottom: 0; }

/* ============================================================ 4. Buttons
   One shape everywhere. See the README for where each one goes. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: var(--control-h);
  padding: 11px 18px;
  font-family: inherit; font-size: 1rem; font-weight: var(--weight-medium);
  border: 1px solid transparent; border-radius: var(--radius);
  background: var(--brand); color: var(--brand-ink);
  cursor: pointer; text-decoration: none; text-align: center;
  transition: background .12s ease, border-color .12s ease, opacity .12s ease;
}
.btn:hover { background: var(--brand-dark); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; transform: none; }

.btn-secondary { background: var(--surface); color: var(--ink); border-color: var(--line); }
.btn-secondary:hover { background: var(--canvas); border-color: var(--ink-faint); }

/* Ghost: the quiet third option. Never the only way to do something important. */
.btn-ghost { background: transparent; color: var(--ink-soft); border-color: transparent; }
.btn-ghost:hover { background: var(--surface); color: var(--ink); }

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

.btn-sm { min-height: var(--control-h-sm); padding: 7px var(--space-3); font-size: var(--text-sm); }
.btn-block { width: 100%; }

/* An icon inside a label keeps its own size and never stretches. */
.btn svg { width: 18px; height: 18px; flex: 0 0 auto; }

/* Busy: the button says so and stops taking presses. Pair with disabled. */
.btn-busy { position: relative; color: transparent !important; pointer-events: none; }
.btn-busy::after {
  content: ""; position: absolute; width: 18px; height: 18px;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  color: var(--brand-ink);
  animation: sui-spin .8s linear infinite;
}
.btn-secondary.btn-busy::after, .btn-ghost.btn-busy::after { color: var(--ink); }
@keyframes sui-spin { to { transform: rotate(360deg); } }

/* Icon-only. Always give it aria-label and title. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: var(--control-h-sm); height: var(--control-h-sm);
  border-radius: 50%; border: 0; cursor: pointer; text-decoration: none;
  background: var(--brand); color: var(--brand-ink);
}
.icon-btn svg { width: 19px; height: 19px; display: block; }
.icon-btn:hover { background: var(--brand-dark); }
.icon-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }
.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); background: var(--surface); }
.icon-btn-danger { background: var(--danger); color: var(--brand-ink); }
.icon-btn-danger:hover { background: var(--danger-hover); }
/* Present but unusable right now: dim it, don't remove it. */
.icon-btn.is-inert, .btn.is-inert { opacity: .38; cursor: default; }
.icon-btn.is-inert:hover { color: inherit; border-color: var(--line); background: var(--surface); }

/* A row of buttons. Primary LAST on the right (see README). */
.btn-row { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.btn-row-end { justify-content: flex-end; }
.btn-row .btn { flex: 0 0 auto; }
/* In a dialog the two fill the width equally. */
.modal-actions { display: flex; gap: var(--space-2); }
.modal-actions .btn { flex: 1; }

/* ============================================================ 5. Forms */
.field { margin-bottom: 14px; }
label { display: block; font-size: var(--text-sm); font-weight: var(--weight-medium); margin-bottom: 5px; }

input[type=text], input[type=password], input[type=email], input[type=tel],
input[type=number], input[type=search], input[type=url],
input[type=date], input[type=time], input[type=datetime-local], input[type=month],
input[type=file], select, textarea {
  width: 100%;
  /* 16px exactly — smaller makes iOS Safari zoom on focus. */
  font-size: 16px;
  font-family: inherit;
  padding: 11px var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  min-height: var(--touch-min);
}
input::placeholder, textarea::placeholder { color: var(--ink-faint); }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--focus-ring);
}
input:disabled, select:disabled, textarea:disabled {
  background: var(--canvas); color: var(--ink-faint); cursor: not-allowed;
}
textarea { min-height: 96px; }

/* Native date and time inputs — the control that catches every portal going
   dark, and the one that differs most between browsers.

   The kit used to draw its own calendar icon here, because Chromium painted a
   picker button and Firefox painted none. Firefox 156 paints one too, so every
   date field showed two icons side by side (found in Work Report, 2026-09-21).
   Worse, the drawn icon cannot be taken back: Firefox exposes no pseudo-element
   for its indicator (::-moz-calendar-picker-indicator does not exist — tested),
   so there is no way to suppress it and keep ours.

   So the kit no longer draws anything. Each browser paints its own indicator
   and `color-scheme` themes it, which is why it looks right in the dark palette
   without a single colour being named here. One icon, always the real control,
   and nothing to re-test when a browser changes its mind again.

   Never put a background-image on these four inputs. That is the rule this
   whole comment exists to state, and uikit_check.py enforces it. */
input[type=date], input[type=time], input[type=datetime-local], input[type=month] {
  background-image: none;
  padding-right: var(--space-3);
}
/* Chromium's button is left visible and sized to match Firefox's. */
input[type=date]::-webkit-calendar-picker-indicator,
input[type=datetime-local]::-webkit-calendar-picker-indicator,
input[type=month]::-webkit-calendar-picker-indicator,
input[type=time]::-webkit-calendar-picker-indicator {
  opacity: 1; cursor: pointer;
  width: 18px; height: 18px;
}
input[type=date]::-webkit-datetime-edit-text,
input[type=date]::-webkit-datetime-edit-month-field,
input[type=date]::-webkit-datetime-edit-day-field,
input[type=date]::-webkit-datetime-edit-year-field { color: var(--ink); }
input[type=date]:invalid::-webkit-datetime-edit { color: var(--ink-faint); }
/* `month` has no picker in Firefox at all — it falls back to a text box. Prefer
   a `date` input, or two selects, if the field matters. */

/* Native select: the browser's own arrow follows color-scheme, so nothing is
   redrawn here. Only the padding-right keeps text clear of it. */
select { padding-right: var(--space-6); }

input[type=checkbox], input[type=radio] { accent-color: var(--brand); width: 18px; height: 18px; }
.check-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-weight: var(--weight-normal); }
.check-row label { margin: 0; font-weight: var(--weight-normal); }

.field-error input, .field-error select, .field-error textarea { border-color: var(--danger); }
input[type=file] { padding: 8px var(--space-3); }
input[type=file]::file-selector-button {
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  margin-right: var(--space-3); padding: 6px var(--space-3);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--canvas); color: var(--ink); cursor: pointer;
}

/* ============================================================ 6. Data */
.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: var(--text-micro); text-transform: uppercase;
  color: var(--ink-soft); letter-spacing: .03em; font-weight: var(--weight-medium);
}
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.table-hover tbody tr:hover { background: var(--brand-tint); }
.table-zebra tbody tr:nth-child(even) { background: var(--canvas); }

/* Status pill. One vocabulary: ok / warn / danger / info / neutral. */
.pill {
  display: inline-block; padding: 3px 10px; border-radius: var(--radius-pill);
  font-size: var(--text-micro); font-weight: var(--weight-strong);
  letter-spacing: .02em; text-transform: uppercase; white-space: nowrap;
}
.pill-ok { background: var(--ok-tint); color: var(--ok); }
.pill-warn { background: var(--warn-tint); color: var(--warn); }
.pill-danger { background: var(--danger-tint); color: var(--danger); }
.pill-info { background: var(--info-tint); color: var(--brand-dark); }
.pill-neutral { background: var(--canvas); color: var(--ink-soft); }
/* Payments' own status names, kept so its markup works unchanged. */
.pill-requested, .pill-approved { background: var(--info-tint); color: var(--brand-dark); }
.pill-processing { background: var(--warn-tint); color: var(--warn); }
.pill-processed, .pill-complete { background: var(--ok-tint); color: var(--ok); }
.pill-rejected { background: var(--danger-tint); color: var(--danger); }

/* Chip: a filter or a removable value. Not a status — that's .pill. */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px; border-radius: var(--radius-pill);
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); font-size: var(--text-sm); cursor: pointer;
}
.chip:hover { border-color: var(--brand); color: var(--ink); }
.chip.on { background: var(--brand-tint); border-color: var(--brand); color: var(--brand-dark); }
.chip svg { width: 14px; height: 14px; }

/* Count on an icon (unread bell). */
.badge {
  position: absolute; transform: translate(12px, -10px);
  background: var(--danger); color: var(--brand-ink);
  font-size: .62rem; font-weight: var(--weight-strong);
  min-width: 17px; height: 17px; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; justify-content: center; padding: 0 4px;
}

.empty { text-align: center; padding: 40px 20px; color: var(--ink-soft); }
.empty svg { width: 34px; height: 34px; opacity: .5; margin-bottom: var(--space-2); }

.pager { display: flex; align-items: center; justify-content: center; gap: var(--space-2); margin-top: var(--space-4); }
.pager .btn { min-width: var(--control-h-sm); }
.pager .pager-at { color: var(--ink-soft); font-size: var(--text-sm); }

/* ============================================================ 7. Feedback */
.alert {
  padding: 11px 14px; border-radius: var(--radius-sm);
  margin-bottom: var(--space-3); 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); }
/* A flash that clears itself. Page notices that must stay are .alert alone. */
.toast { transition: opacity .28s ease, transform .28s ease; }
.toast-gone { opacity: 0; transform: translateY(-4px); }

.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(var(--glass-blur)) saturate(150%); backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  border: 1px solid var(--glass-edge); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 18px 18px var(--space-4);
}
.modal-glass h2 { margin: 0 0 6px; font-size: 1.05rem; }
.modal-glass p { margin: 0 0 var(--space-4); color: var(--ink-soft); font-size: .88rem; }

/* Covers the page while something is in flight. No fade: a transition does
   not advance in a window the browser is not painting. */
.busy-overlay {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim); cursor: progress;
  -webkit-user-select: none; user-select: none;
}
.busy-overlay[hidden] { display: none; }
.busy-panel {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 14px 20px;
  background: var(--glass);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%); backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  border: 1px solid var(--glass-edge); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); font-weight: var(--weight-medium);
}
.busy-spinner {
  width: 20px; height: 20px; flex: none;
  border: 3px solid var(--line); border-top-color: var(--brand);
  border-radius: 50%; animation: sui-spin .8s linear infinite;
}

/* ============================================================ 8. Navigation */
/* Tabs. Not in Payments today — added here because the other portals have
   them; built from the same tokens. */
.tabs { display: flex; gap: var(--space-4); border-bottom: 1px solid var(--line); margin-bottom: var(--space-4); overflow-x: auto; }
.tabs a, .tabs button {
  appearance: none; background: none; border: 0; cursor: pointer;
  font: inherit; font-size: var(--text-sm); font-weight: var(--weight-medium);
  color: var(--ink-soft); text-decoration: none;
  padding: 10px 2px; border-bottom: 2px solid transparent; white-space: nowrap;
}
.tabs a:hover, .tabs button:hover { color: var(--ink); }
.tabs .active { color: var(--brand); border-bottom-color: var(--brand); }

/* Segmented control: two to four mutually exclusive choices, one visible
   group. Payments' theme switch is the original. */
.segment {
  display: inline-flex; gap: 3px; padding: 2px;
  border: 1px solid var(--line); border-radius: 20px; background: var(--surface);
}
.segment button, .segment a {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  appearance: none; border: 0; background: none; cursor: pointer; text-decoration: none;
  font: inherit; font-size: var(--text-sm); color: var(--ink-soft);
  min-height: 32px; padding: 0 14px; border-radius: var(--radius-pill);
}
.segment .on { background: var(--brand); color: var(--brand-ink); font-weight: var(--weight-medium); }
.segment button:focus-visible, .segment a:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }

/* ============================================================ 9. Sign-in page */
.auth-shell {
  min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: var(--space-5) var(--space-4);
}
.auth-logo { height: 34px; margin-bottom: 18px; }
.auth-card { width: 100%; max-width: 400px; }
.auth-footer { margin-top: 18px; text-align: center; font-size: var(--text-xs); color: var(--ink-faint); }
/* "or" between two ways to sign in. */
.or-divider { display: flex; align-items: center; gap: 10px; margin: var(--space-4) 0 var(--space-3); color: var(--ink-soft); font-size: var(--text-xs); }
.or-divider::before, .or-divider::after { content: ""; flex: 1; border-top: 1px solid var(--line); }

/* ============================================================ 10. Print
   Paper is white. Print black on white, drop the furniture, and let tables
   break across pages. */
@media print {
  :root { color-scheme: light; }
  body { background: #fff; color: #000; }
  .app-header, .bottom-nav, .app-dock, .btn, .icon-btn, .pager, .tabs, .segment,
  .busy-overlay, .modal-backdrop, .no-print { display: none !important; }
  .card { border: 1px solid #999; box-shadow: none; break-inside: avoid; background: #fff; }
  .page { max-width: none; padding: 0; }
  a { color: #000; text-decoration: underline; }
  th, td { border-bottom: 1px solid #999; }
  th { color: #000; }
  .pill { border: 1px solid #666; color: #000 !important; background: #fff !important; }
  .muted, .faint, .hint, .kv dt { color: #333; }
  table { page-break-inside: auto; }
  tr { page-break-inside: avoid; }
  thead { display: table-header-group; }
}

/* ============================================================ Narrow screens */
@media (max-width: 460px) {
  .page { padding-left: var(--space-3); padding-right: var(--space-3); }
  .icon-btn { width: 36px; height: 36px; }
  .icon-btn svg { width: 18px; height: 18px; }
  /* Stacked, full width, primary first: on a phone the thumb is at the bottom
     and a row of two wraps awkwardly. */
  .btn-row-stack { flex-direction: column-reverse; }
  .btn-row-stack .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* ============================================================ 8b. Theme switch
   The three-way control. Markup and script: see README "The shared theme".
   Works with JavaScript off if the form posts to the portal's own endpoint. */
.theme-switch {
  --ts-size: 32px; --ts-gap: 3px; --ts-pos: 0;
  position: relative;
  display: inline-flex; gap: var(--ts-gap); padding: 2px;
  border: 1px solid var(--line); border-radius: 20px; background: var(--surface);
}
.theme-switch button {
  display: inline-flex; align-items: center; justify-content: center;
  appearance: none; border: 0; background: none; cursor: pointer;
  color: var(--ink-soft); padding: 0; border-radius: 50%;
  /* Deliberately below the 44px floor: a rarely-used preference in a header
     beside real actions, square so the lit state is a disc not a lozenge. */
  width: var(--ts-size); height: var(--ts-size); min-height: 0;
  position: relative; z-index: 1;   /* above the sliding thumb */
}
.theme-switch button svg { width: 17px; height: 17px; display: block; }
.theme-switch button:hover { color: var(--ink); }
.theme-switch button.on { color: var(--brand-ink); }
.theme-switch button:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-ring); }

/* The lit disc is ONE element that slides to whichever button is on, rather
   than a background that jumps between them. Its own view-transition-name, so
   during the page cross-fade the browser moves it instead of fading it.
   Positioned with :has(); a browser without :has() keeps the plain lit button. */
.theme-thumb {
  position: absolute; top: 2px; left: 2px;
  width: var(--ts-size); height: var(--ts-size); border-radius: 50%;
  background: var(--brand);
  transform: translateX(calc(var(--ts-pos) * (var(--ts-size) + var(--ts-gap))));
  /* No transition of its own: the view transition slides it, and where there
     is none .theme-switching does. Both at once photograph it half-moved. */
  view-transition-name: theme-thumb;
}
.theme-switch:has(button.on[value="auto"]) { --ts-pos: 1; }
.theme-switch:has(button.on[value="dark"]) { --ts-pos: 2; }
@supports not selector(:has(a)) {
  .theme-thumb { display: none; }
  .theme-switch button.on { background: var(--brand); }
}

/* Switching the theme eases the colours rather than snapping. Scoped three
   ways so nothing else in the portal picks up a transition it did not ask for:
   only while .theme-switching is on <html>, only the properties that actually
   change colour, and never for someone who asked for less motion. It is added
   by sanrad-theme.js at the moment of the switch and removed afterwards, so it
   is never present on first paint — a page load must not fade in. */
@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, box-shadow .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; }
}

/* Notification bell and its panel. */
.bell { position: relative; }
.bell-panel {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  width: min(360px, calc(100vw - 32px));
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  max-height: 60vh; overflow-y: auto;
}
.bell-panel .list-item { padding: var(--space-3) var(--space-4); }
.bell-panel-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; background: var(--surface);
}

/* Unread: a left bar AND a dot, never colour alone (read in sunlight). */
.is-unread { border-left: 3px solid var(--brand); padding-left: var(--space-3); }
.is-unread .unread-dot,
.unread-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--brand); flex: 0 0 auto; }

/* Bookmark bar: a row of saved links above the content. */
.bookmarks { display: flex; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-2) 0 var(--space-3); }

/* ============================================================ 7b. Progress */
.progress { height: 6px; border-radius: var(--radius-pill); background: var(--line); overflow: hidden; }
.progress-bar { height: 100%; background: var(--brand); border-radius: inherit; transition: width .2s ease; }
/* No value known: a stripe that travels. */
.progress-indeterminate .progress-bar {
  width: 35%; animation: sui-slide 1.1s ease-in-out infinite;
}
@keyframes sui-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
.busy-panel .progress { width: 160px; }

/* ============================================================ 11. Capture */
/* Signature pad. The canvas is sized by CSS here but MUST also carry width and
   height attributes set in script from its own box and devicePixelRatio, or
   the drawing is stretched. Keep padding at 0 and box-sizing content-box on
   the canvas itself: any padding scales the drawing surface away from the
   pointer coordinates. */
.sigpad { border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); overflow: hidden; }
.sigpad canvas {
  display: block; width: 100%; height: 180px; padding: 0; box-sizing: content-box;
  touch-action: none; cursor: crosshair; background: transparent;
}
/* The line people sign on, drawn in CSS so it never lands in the saved image. */
.sigpad-line { position: relative; }
.sigpad-line::after {
  content: ""; position: absolute; left: 12%; right: 12%; bottom: 34px;
  border-bottom: 1px dashed var(--line); pointer-events: none;
}
.sigpad-actions { display: flex; gap: var(--space-2); justify-content: flex-end; padding: var(--space-2); border-top: 1px solid var(--line); }

/* Camera capture: live video, then the still. Same box either way, so the
   panel does not jump when the photo is taken. */
.capture { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.capture-frame { position: relative; aspect-ratio: 4 / 3; background: var(--canvas); }
.capture-frame video, .capture-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.capture-actions { display: flex; gap: var(--space-2); padding: var(--space-3); justify-content: center; }
.capture-hint { position: absolute; left: 0; right: 0; bottom: 0; padding: 6px var(--space-3); font-size: var(--text-xs); color: var(--brand-ink); background: rgba(0, 0, 0, .45); }

/* QR scanner: the library injects its own video and canvas, so only the box
   is styled here — no rules reach inside. */
.qr-box { position: relative; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: #000; aspect-ratio: 1 / 1; }
.qr-box > * { width: 100% !important; }
.qr-reticle { position: absolute; inset: 12%; border: 2px solid var(--brand); border-radius: var(--radius); box-shadow: 0 0 0 100vmax var(--scrim); pointer-events: none; }

/* Card-scan target for a shared terminal: readable at arm's length. */
.scan-target {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-4);
  min-height: 320px; padding: var(--space-6);
  border: 2px dashed var(--line); border-radius: var(--radius);
  background: var(--surface); text-align: center;
}
.scan-target svg { width: 72px; height: 72px; color: var(--ink-soft); }
.scan-target .scan-title { font-size: 1.5rem; font-weight: var(--weight-medium); }
.scan-target .scan-sub { color: var(--ink-soft); }
.scan-target.is-scanning { border-color: var(--brand); border-style: solid; }
.scan-target.is-scanning svg { color: var(--brand); animation: sui-pulse 1.2s ease-in-out infinite; }
.scan-target.is-error { border-color: var(--danger); border-style: solid; background: var(--danger-tint); }
.scan-target.is-error svg, .scan-target.is-error .scan-title { color: var(--danger); }
@keyframes sui-pulse { 50% { opacity: .45; } }

/* Photo grid with a picker and a limit. */
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--space-2); }
.photo-grid figure { margin: 0; position: relative; aspect-ratio: 1 / 1; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--line); background: var(--canvas); }
:where(.photo-grid) img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-grid .photo-remove { position: absolute; top: 4px; right: 4px; width: 26px; height: 26px; }
.photo-add {
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 1 / 1; border: 1px dashed var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink-soft); cursor: pointer;
}
.photo-add:hover { border-color: var(--brand); color: var(--brand); }
.photo-count { font-size: var(--text-xs); color: var(--ink-soft); margin-top: var(--space-2); }

/* ============================================================ 12. Dense UI */
/* Permission grid: many checkboxes, some not available to this role. */
.grid-matrix { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.grid-matrix th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
.grid-matrix td { text-align: center; }
.grid-matrix td:first-child, .grid-matrix th:first-child { text-align: left; }
.grid-matrix tbody tr:hover { background: var(--brand-tint); }
.grid-matrix .cell-off { background: var(--canvas); }
.grid-matrix .cell-off::after { content: "—"; color: var(--ink-faint); }
.grid-matrix .cell-off input { display: none; }

/* A masked value with a reveal control (KYC rows). */
.masked { display: inline-flex; align-items: center; gap: var(--space-2); font-family: var(--font-mono); }
.masked-value { letter-spacing: .08em; }
.masked[data-revealed="false"] .masked-value { color: var(--ink-soft); }

/* Shown once and never again: a voucher code or a generated password. */
.secret {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--brand); border-radius: var(--radius);
  background: var(--brand-tint);
}
.secret code { font-family: var(--font-mono); font-size: 1.1rem; letter-spacing: .06em; color: var(--ink); word-break: break-all; }
.secret .secret-note { font-size: var(--text-xs); color: var(--ink-soft); }

/* Steps in a multi-step form. The number is the non-colour cue; done gets a tick. */
.steps { display: flex; gap: var(--space-2); overflow-x: auto; padding-bottom: var(--space-2); }
.step { display: flex; align-items: center; gap: var(--space-2); flex: 0 0 auto; color: var(--ink-soft); font-size: var(--text-sm); }
.step-dot {
  width: 26px; height: 26px; border-radius: 50%; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); background: var(--surface); font-size: var(--text-xs); font-weight: var(--weight-medium);
}
.step.is-current { color: var(--ink); font-weight: var(--weight-medium); }
.step.is-current .step-dot { border-color: var(--brand); background: var(--brand); color: var(--brand-ink); }
.step.is-done .step-dot { border-color: var(--ok); color: var(--ok); }
.step + .step::before { content: ""; width: 18px; border-top: 1px solid var(--line); }

/* Consent: scrollable terms, then a typed signature. */
.terms-box {
  max-height: 260px; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4); background: var(--surface);
  font-size: var(--text-sm); line-height: 1.6;
}
.consent-sign { margin-top: var(--space-3); }
.consent-sign input { font-family: var(--font-mono); }

/* Filters that live in a table header. */
.filter-bar { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
.filter-pop {
  position: absolute; z-index: 40; margin-top: var(--space-2);
  min-width: 240px; padding: var(--space-3);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
}
.filter-pop label { font-weight: var(--weight-normal); }
.filter-row th { padding-top: 0; }
.filter-row input, .filter-row select { width: 100%; min-height: 34px; padding: 5px 8px; font-size: var(--text-xs); }

/* A WhatsApp-style preview of an outgoing message. Deliberately not themed:
   it is a picture of another application, so it keeps its own colours. */
.msg-preview { background: #e5ddd5; padding: var(--space-4); border-radius: var(--radius); }
.msg-bubble {
  max-width: 420px; margin-left: auto; padding: 8px 10px;
  background: #dcf8c6; color: #111b21; border-radius: 8px 8px 0 8px;
  font-size: .9rem; line-height: 1.45; white-space: pre-wrap; word-break: break-word;
  box-shadow: 0 1px 1px rgba(0, 0, 0, .13);
}
.msg-bubble .msg-time { display: block; text-align: right; font-size: .68rem; color: #667781; margin-top: 2px; }

/* A document-style report: numbered sections, data tables, signature blocks.
   Built to print. */
.report { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--space-5); }
.report h2 { counter-increment: sui-section; margin-top: var(--space-5); }
.report h2::before { content: counter(sui-section) ". "; color: var(--ink-soft); }
.report { counter-reset: sui-section; }
.report table { margin: var(--space-3) 0; }
.sign-blocks { display: flex; gap: var(--space-5); flex-wrap: wrap; margin-top: var(--space-6); }
.sign-block { flex: 1 1 200px; }
.sign-block .sign-line { border-top: 1px solid var(--ink-soft); margin-top: 48px; padding-top: 6px; font-size: var(--text-xs); color: var(--ink-soft); }

/* A wide table on a phone: hide the table, show one card per row. Give each
   cell data-label="Column" and the label appears beside the value. */
@media (max-width: 640px) {
  .table-cards thead { display: none; }
  .table-cards, .table-cards tbody, .table-cards tr, .table-cards td { display: block; width: 100%; }
  .table-cards tr {
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--surface); margin-bottom: var(--space-2); padding: var(--space-2) var(--space-3);
  }
  .table-cards td { border: 0; padding: 5px 0; display: flex; justify-content: space-between; gap: var(--space-3); }
  .table-cards td::before { content: attr(data-label); color: var(--ink-soft); font-size: var(--text-micro); text-transform: uppercase; letter-spacing: .03em; }
  .table-cards td.num { text-align: right; }
}

/* ============================================================ Status family
   One vocabulary for every portal. Colour is never the only cue: each state
   carries a glyph, so it reads in sunlight and to a colour-blind eye. */
.pill::before { margin-right: 5px; font-weight: var(--weight-strong); }
.pill-ok::before, .pill-complete::before, .pill-processed::before,
.pill-verified::before, .pill-active::before, .pill-signed::before { content: "\2713"; }        /* tick */
.pill-warn::before, .pill-processing::before, .pill-pending::before,
.pill-submitted::before, .pill-awaiting::before, .pill-pending-review::before { content: "\25CF"; } /* dot */
.pill-danger::before, .pill-rejected::before, .pill-suspended::before,
.pill-expired::before, .pill-disabled::before { content: "\2715"; }                              /* cross */
.pill-info::before, .pill-approved::before, .pill-requested::before { content: "\21B3"; }        /* arrow */
.pill-neutral::before, .pill-draft::before { content: "\25CB"; }                                 /* hollow */
/* The states every portal shares, mapped onto the five colours. */
.pill-pending, .pill-submitted, .pill-awaiting, .pill-pending-review { background: var(--warn-tint); color: var(--warn); }
.pill-verified, .pill-active, .pill-signed { background: var(--ok-tint); color: var(--ok); }
.pill-suspended, .pill-expired, .pill-disabled { background: var(--danger-tint); color: var(--danger); }
.pill-draft { background: var(--canvas); color: var(--ink-soft); }

/* Form errors carry a glyph too, not just a red border. */
.error-text::before { content: "\26A0"; margin-right: 5px; }

/* ============================================================ 9b. Public pages
   Signed-out pages: no navigation, one panel, the logo above it. */
.public-shell { min-height: 100dvh; display: flex; flex-direction: column; align-items: center; padding: var(--space-6) var(--space-4); }
.public-shell .page { width: 100%; }
.public-head { text-align: center; margin-bottom: var(--space-5); }
:where(.public-head) img { height: 34px; }


/* ============================================================ 13. Brand, nav, footer
   The logo is used exactly as supplied: never recoloured, redrawn, re-exported,
   tinted or filtered. The colours are by design.

   It therefore needs no area of its own. The artwork carries its own blue tile
   and was drawn for a light background, so .brand is layout only -- spacing,
   alignment and the image height -- and the logo sits on the surface underneath,
   which is what Payments itself has always done. Until 1.2.2 this rule painted
   a dark block behind it in every theme; see the token comment at the top. */
/* Where :where() is used, and where it is not. The kit sizes YOUR content --
   logos, header images, gallery photos -- as a default you can override with a
   single class. It sizes its OWN components -- the theme switch's disc, the
   signature pad, the capture frame, icons inside .btn -- normally, because
   those measurements are what make the component work; a portal that wants a
   different one is changing the component, and should say so with its own
   specificity. */
.brand {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 6px 14px; border-radius: var(--radius);
  color: var(--ink);          /* .brand-name beside the logo, legible in both themes */
  text-decoration: none;
}
/* The height is wrapped in :where() so it has the weight of a bare `img`
   (0,0,1) and ANY portal rule with one class -- .brand-logo-img { height: 28px }
   -- outranks it. Before 1.2.2 this was `.brand img` (0,1,1), which silently
   shrank a portal's own logo when it took the kit. SLA measured it: 28px on
   1.1.1, 26px on 1.2.1, with no rule of theirs changed. Same for the variants
   below: kit sizing is a default, never an override. */
:where(.brand) img, :where(.brand) svg { height: 26px; width: auto; display: block; }
.brand-name { font-weight: var(--weight-medium); letter-spacing: -.01em; }
/* In the header the block sits at the left, before everything else. */
:where(.app-header) .brand { margin-right: var(--space-2); }
/* Signed-out pages: the same block, centred above the card, a little larger. */
:where(.auth-shell, .public-head) .brand { padding: 10px 18px; margin-bottom: 18px; }
:where(.auth-shell, .public-head) :where(.brand) img { height: 30px; }

/* The portal's own links, beside the brand in the header. */
.app-nav { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
.app-nav a {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px var(--space-3); border-radius: var(--radius-pill);
  color: var(--ink-soft); text-decoration: none; font-size: var(--text-sm);
  font-weight: var(--weight-medium); min-height: var(--control-h-sm);
}
.app-nav a:hover { background: var(--canvas); color: var(--ink); }
.app-nav a.active { background: var(--brand-tint); color: var(--brand-dark); }
.app-nav a svg { width: 17px; height: 17px; }

/* One line at the foot of every page. */
/* ---- The page shell, so the footer sits at the bottom of a short page.

   OPT-IN. Put .app-shell on the element that wraps header + content + footer.
   A portal that does not use it is unaffected -- that is the whole point, and
   why this is not a rule on `body`: Hotspot's footer is already fixed and feeds
   --footer-h into their page frame, and nobody's layout should change because
   somebody else asked for this.

   What it does: on a short page the footer sits at the bottom of the viewport
   instead of floating mid-screen above a band of empty canvas, which reads as a
   page that failed to load. On a long page it simply follows the content.

   What it must NOT do: position: fixed. A fixed footer overlays the last rows
   of a long table -- a worse bug than the one being fixed.

   NOTHING NEEDS TO BE MARKED AS THE GROWING CHILD. The footer takes the free
   space above it with margin-top:auto, so a portal cannot forget a step and get
   a shell that silently does nothing. A designated content element would be one
   more instruction to follow loosely; this is structure.

   100dvh, not 100vh: on a phone, vh includes the browser chrome and the footer
   ends up under the address bar.

   min-height only, never height: a page whose content is exactly viewport-tall,
   or one holding a table with its own overflow container, must be free to grow.
   Setting height here is what clips a 1080px voucher table. */
.app-shell {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}
/* Every direct child spans the column. Without this, a centred child collapses
   to its CONTENT width: `.page` is `max-width: 760px; margin: 0 auto`, and in a
   flex column the auto side margins absorb the free space instead of stretching
   the item. Work Report measured their home page at 352px instead of 728px, and
   a 900px window here gives 227px. `.page` keeps its own max-width and stays
   centred; this only stops it collapsing.

   A WIDE TABLE HIDES THIS ENTIRELY -- it fills the column, so there is no free
   space for the margins to eat. Any test of this class must include a narrow
   centred page AND assert its WIDTH. My own first version of these checks used
   a narrow page and still missed it, because every assertion was about vertical
   position. The page shape was right; the measurements were incomplete. */
.app-shell > * { width: 100%; }
.app-shell > .app-footer { margin-top: auto; }
/* .auth-shell and .public-shell are ALREADY full-height, so signed-out pages
   need nothing. One inside .app-shell would be a second 100dvh box -- a page
   twice the viewport, with the footer pushed off the bottom: the same bug,
   arriving as a consequence of the fix. Made impossible here rather than
   forbidden in a sentence. */
.app-shell .auth-shell, .app-shell .public-shell { min-height: 0; }

.app-footer {
  padding: var(--space-4) var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
  color: var(--ink-faint); font-size: var(--text-xs); text-align: center;
}
.app-footer a { color: var(--ink-soft); }

@media (max-width: 460px) {
  .brand { padding: 5px 10px; }
  :where(.brand) img, :where(.brand) svg { height: 22px; }
  /* Restated so the narrow-screen rule does not quietly become the winner on a
     sign-in page now that both are 0,0,1: this is the same 30px as wide. */
  :where(.auth-shell, .public-head) :where(.brand) img { height: 30px; }
  .brand-name { display: none; }   /* the wordmark carries the name already */
  .app-nav { gap: 0; }
  .app-nav a { padding: 8px 10px; }
}
