/*
 * Sanrad Hotspot Manager -- mobile-first stylesheet.
 *
 * Built for one-handed phone use first: 16px minimum input text (anything
 * smaller makes iOS Safari zoom on focus) and 44px minimum tap targets. The
 * desktop layout widens the same structure rather than being a separate design.
 *
 *
 *
 * ---------------------------------------------------------------------------
 * RULES THE KIT AIMS AT OUR MARKUP WITHOUT SHARING A NAME
 * ---------------------------------------------------------------------------
 * A name-for-name audit cannot see `.brand img` or `.modal-glass p`: no name
 * of ours appears in them, and they restyle our elements anyway. Nor can it
 * see a class our TEMPLATES use that only the kit styles. Both are checked
 * with scripts/kit-audit.py, which also watches background and padding --
 * a background on a container is not colour noise, it draws a visible box.
 *
 * Counted at kit 1.2.0 (19 such rules); the kit is now 1.4.0, so re-run
 * scripts/kit-audit.py rather than trusting that number. The ones that
 * matter here:
 *   .brand            THE ONE TO WATCH. The kit paints a coloured plaque
 *                     behind the whole brand block (background + padding +
 *                     radius). Our rule sets neither, so the plaque WOULD
 *                     appear -- our logo is a wordmark with its own ground
 *                     and is used as supplied, never on a second ground.
 *
 * MEASURED, 2026-09-21, and worth keeping: the header's height is set by its
 * TALLEST item, the theme toggle at ~38px. The brand block -- our 20px logo
 * plus the kit's 6px padding top and bottom -- comes to 32px, so it fits
 * inside with 6px to spare and does not touch --header-h. Verified by
 * capturing /admin under two kit versions: header ends y=57, card starts
 * y=87, shift zero. THE BRAND BLOCK IS 6px SHORT OF SETTING THE HEADER
 * HEIGHT. Shrink the theme toggle and the brand becomes the tallest thing,
 * --header-h grows, and every page below it moves.
 *   .brand img        kit 26px vs our 20px; our `.app-header .brand .logo` is
 *                     more specific, so ours wins. Recheck if that changes.
 *   .app-header img   kit 28px; same, ours is more specific.
 *   .scan-target svg  ADOPTED the kit's 72px in slice 9 (the terminal); our
 *                     56px rule is gone.
 *   .empty svg, .btn svg, .modal-glass h2/p, .step + .step::before,
 *   .step.is-current .step-dot  -- nothing of ours competes; they apply as
 *                     soon as that markup exists. Reviewed by the owning
 *                     slice, not assumed.
 * ---------------------------------------------------------------------------
 * NAMES THE KIT ALSO DEFINES -- check here before inventing one
 * ---------------------------------------------------------------------------
 * Both stylesheets define class names in common (30 when first counted; 21
 * top-level names at kit 1.4.0 after slice 2). This file loads second, so our
 * declarations win -- but any property we do NOT set falls through to the
 * kit's. That is silent: no error, no failing test. It is how the card-scan
 * target grew taller the moment the kit arrived.
 *
 * Re-run the audit after taking a new kit version:
 *   comm -12 <(grep -oE "^\.[a-z][a-z0-9-]*" static/css/portal.css | sort -u) \
 *            <(grep -oE "^\.[a-z][a-z0-9-]*" ../ui-kit/sanrad-ui.css | sort -u)
 *
 * DELETED in slice 2 (2026-09-21) -- ours duplicated the kit's with different
 * numbers:
 *   .alert .alert-error .alert-info .alert-success .alert-warning
 *   .card .field .small .muted .empty .btn-sm .modal-actions .modal-backdrop
 *   .pill .pill-ok .pill-warn .pill-danger
 * What remains under some of those names here is descendant or state rules
 * (.card h2, .field > label, .modal-backdrop.open, .modal-actions .btn), not
 * a second definition of the component. .btn-sm came back as the
 * .btn.btn-sm anchor below, for the cascade reason given there.
 *
 * KEEP, with the reason:
 *   .app-header  ours is position:fixed and feeds --header-h, which misc.js
 *                measures and the page frame reserves. The kit's is sticky;
 *                adopting it would move the layout, and layouts stay put.
 *   .app-footer  same: fixed to the viewport, and --footer-h reserves it.
 *   .bell        the kit's rule is only position:relative, a hook for its
 *                panel. Ours is the button itself. Not a conflict.
 *   .modal-glass ours is 460px and scrolls at 86vh because our dialogs hold
 *                forms; the kit's 380px is for confirmations.
 *   .page        ours is 1080px for the voucher and audit tables; the kit's
 *                760px is a reading column.
 *
 * DECIDED IN SLICE 6 (admin screens), 2026-09-21:
 *   .btn-danger  ADOPTED the kit's filled red; our outline override is gone.
 *                Every destructive button in the estate now looks the same,
 *                including the glass modal's confirm (modal.js uses it).
 *   .btn .btn-primary  KEPT, names only. Ours invert the kit's convention
 *                (our plain .btn is the quiet one, .btn-primary the filled
 *                one; the kit's plain .btn is filled). The LOOK already
 *                matches, so renaming 100-odd buttons across every template
 *                would buy no visible change for a large, easily-broken diff.
 *   .table-wrap  KEPT, with the reason: the kit's pattern puts the table
 *                inside a .card, which draws the frame. Ours stand alone,
 *                so our rule plays the card's part. Wrapping every table in a
 *                card would move the layout, and layouts stay put.
 *   .btn-row     WAS a NAME COLLISION, not a variant, and is no longer
 *                used here. Ours was a four-column grid for editing WhatsApp
 *                call-to-action buttons; the kit's is a flex row of buttons.
 *                Harmless while our display:grid won, but the same shape as
 *                the `.empty` meter bug. Renamed to .cta-row in slice 8
 *                (message previews), which owns that screen. The kit's
 *                .btn-row is free for its own use.
 *
 * KEEP UNTIL THE SLICE THAT OWNS IT, then decide with the screen in front of
 * you -- deliberate overrides, not forgotten ones:
 *   .step .steps .step-dot      KEPT by slice 7 (public pages, 2026-09-21):
 *                               the kit's stepper is a different design, not
 *                               a restyle of ours, and adopting it would be a
 *                               redesign nobody has asked for
 *   .table-wrap                 KEPT by slice 6 -- see above
 *   .scan-target                ADOPTED by slice 9 (terminal): the kit's box
 *                               and states; ours keeps only its margin
 * ---------------------------------------------------------------------------
 * Loaded AFTER sanrad-ui.css, the shared kit every Sanrad portal uses, so
 * what is here either adds to the kit or deliberately overrides it. The kit
 * owns the palette, type, spacing, radii, shadows and glass; this file owns
 * the page frame and the screens that exist only in this portal.
 *
 * Theming has three states, not two. An explicit choice stamps
 * data-theme="light|dark" on <html>; 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.
 */

/* What this portal needs on top of the kit.
 *
 * sanrad-ui.css defines the palette, type, spacing, radii, shadows and glass
 * for every Sanrad portal, in all three theme states. Nothing here repeats
 * one of those: what is below is either a measurement of *this* portal's page
 * frame, or a colour the kit has no opinion about.
 */
:root {
  /* --- Names the kit does not have, bridged so our rules keep reading ---- */
  /* The hover fill. The kit calls it --brand-dark; the name here is older
     and still used by links, tiles, the meter and the primary button. */
  --brand-bright: var(--brand-dark);
  /* A divider that has to carry more weight than --line: the edge of a
     table header, a section rule. The kit has per-status edges but no
     neutral strong line. */
  --line-strong: #bcc9d8;
  /* A raised row against --surface. The kit has .table-zebra instead, but
     slice 6 kept our tables (see .table-wrap above), and our rules use this
     colour in many places, so it stays. */
  --surface-2: #eef2f7;

  /* --- This portal's page frame ----------------------------------------- */
  /* The masthead's height. The nav bar sticks directly beneath it, so this
     has to be right or the two overlap or leave a gap. A sensible default is
     written here and corrected from the real rendered height by misc.js --
     which matters because a late web font can change it after first paint. */
  --header-h: 59px;
  /* The nav bar beneath it. Zero on public pages, which have no nav -- also
     kept in step from misc.js. */
  --nav-h: 58px;
  /* Everything pinned to the top, together. What content must clear. */
  --chrome-h: calc(var(--header-h) + var(--nav-h));
  /* The copyright strip is fixed to the viewport, so it is out of flow and
     cannot push content down by itself. This height is what `body` reserves
     underneath -- change one and you must change the other, which is why it
     is a token rather than a number written twice. */
  --footer-h: 44px;
  /* Breathing room between the content and the pinned strips, top and bottom,
     so nothing ever sits hard against them. One value for both ends, so the
     page is symmetrically inset rather than accidentally lopsided. */
  --chrome-gap: 28px;
  --footer-gap: var(--chrome-gap);

  /* --- The frosted strips' tint ---------------------------------------- */
  /* Fixed values. Until 2026-09-21 an admin could tune the opacity and blur
     under System Settings > Appearance; that screen and its two settings
     were removed (migration syssettings/0005), so these are the only source.
     The blur is the kit's --glass-blur. */
  --glass-rgb: 255, 255, 255;
  --glass-bar-alpha: .42;
  --glass-bar: rgba(var(--glass-rgb), clamp(.25, var(--glass-bar-alpha), 1));
}


/* Auto: nothing stamped, and the browser says dark. Guarded exactly as the
   kit guards its own block, so a pinned light choice still wins. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --line-strong: #3a4a59;
    --surface-2: #1c2632;
    --glass-rgb: 18, 25, 33;
  }
}

:root[data-theme="dark"] {
  --line-strong: #3a4a59;
  --surface-2: #1c2632;
  --glass-rgb: 18, 25, 33;
}


* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--canvas);
  -webkit-text-size-adjust: 100%;
  /* Clearance for the pinned strips at both ends. All three are fixed, so they
     are out of flow and cannot make room for themselves -- content would start
     underneath the masthead and end underneath the copyright strip without
     this. */
  padding-top: calc(var(--chrome-h) + var(--chrome-gap));
  padding-bottom: calc(var(--footer-h) + var(--footer-gap) + env(safe-area-inset-bottom));
}

/* Links.
   Without this, a plain <a> falls back to the browser default #0000EE, which
   is unreadable on the dark ground -- exactly what happened to the "sign in"
   and "terminal" links on the landing page. Components that carry their own
   colour (.btn, .tile, .nav a, .brand) are class-based and so still win. */
a { color: var(--brand); text-underline-offset: 2px; }
a:hover { color: var(--brand-bright); }
a:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* ---------- layout ---------- */
/* Fixed rather than sticky: content passes beneath it, which is what the glass
   is for. Opaque by default so the bar is readable where backdrop-filter is
   unsupported; the frost is added only where the browser can actually blur. */
.app-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 31;
  display: flex; align-items: center; gap: 14px;
  padding: 10px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.app-header .brand {
  font-weight: 600; letter-spacing: -.01em; color: var(--ink);
  text-decoration: none; display: flex; align-items: center; gap: 9px;
}
/* The Sanrad wordmark, carried over from the payments portal and used exactly
   as supplied. It carries its own blue ground behind the white "Sa" and sets
   "nrad" in brand blue on transparency, so it reads on a light or a dark
   surface without a per-theme variant and without any filter. Do not recolour
   or key it -- the white is part of the mark, by design. */
.app-header .brand .logo {
  height: 20px; width: auto; display: block; flex: none;
}
/* Separates the company mark from the product name without a heavy rule. */
.app-header .brand .wordmark {
  padding-left: 10px; margin-left: 2px;
  border-left: 1px solid var(--line-strong);
  color: var(--ink-soft); font-weight: 500;
}
.app-header .spacer { margin-left: auto; }
/* A phone, signed in: logo, product name, theme switch, bell and Sign out are
   about 440px wide in a 390px screen. The header is FIXED, so what spills off
   the right cannot be scrolled to: until 2026-09-21 the bell and Sign out
   were simply unreachable on a phone. Here the product name goes (the logo
   stays) and the gaps close up, so everything fits. Only when the header
   carries the account controls (:has the Sign out button); a signed-out
   header has room and keeps its name. */
@media (max-width: 480px) {
  .app-header:has(.icon-btn-danger) { gap: 8px; }
  .app-header:has(.icon-btn-danger) .brand .wordmark { display: none; }
}

/* The top inset is the gap below the sticky chrome; the bottom clearance for
   the fixed copyright strip lives on `body`, since a page may put content
   outside .page. */
.page { max-width: 1080px; margin: 0 auto; padding: 0 16px 12px; }
.page.narrow { max-width: 560px; }
/* Table screens take the whole viewport. A log with ten columns squeezed into
   a 1080px column scrolls sideways on a monitor with room to spare, and these
   pages exist to be scanned across. Only the side gutter is kept. */
.page.wide { max-width: none; padding-left: 24px; padding-right: 24px; }
@media (min-width: 1600px) { .page.wide { padding-left: 32px; padding-right: 32px; } }

/* Sticks directly under the masthead so the sections stay reachable however
   far a table scrolls. Opaque, and one below the header in the stack, so a
   sideways-scrolled nav never bleeds over it. */
.nav {
  position: fixed; top: var(--header-h); left: 0; right: 0; z-index: 30;
  display: flex; align-items: center; gap: 2px; padding: 0 16px;
  background: var(--surface); border-bottom: 1px solid var(--line);
  /* Deliberately NOT overflow-x: auto. The page menu hangs out of the bottom
     of this bar, and any overflow value other than visible on an ancestor
     clips it -- the dropdown opened but rendered invisible. Sideways scrolling
     for a long bookmark bar lives on .nav-links, which contains no popup. */
}
.nav-links { display: flex; gap: 2px; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.nav-links::-webkit-scrollbar { display: none; }
/* The same underlined-tab idiom as the kit's `.tabs`, in the kit's type and
   spacing tokens -- but NOT the kit's `.tabs` class. That rule puts
   `overflow-x: auto` on the container, and any overflow other than visible on
   an ancestor clips the page menu that hangs out of the bottom of this bar:
   the dropdown opens and renders invisible. The bar solved that once already;
   adopting the class wholesale would walk straight back into it. */
.nav a {
  padding: 11px var(--space-3); font-size: var(--text-sm); text-decoration: none;
  color: var(--ink-soft); white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.nav a:hover { color: var(--ink); }
.nav a.on {
  color: var(--brand); border-bottom-color: var(--brand);
  font-weight: var(--weight-medium);
}
.nav-empty { padding: 11px 4px; font-size: .82rem; color: var(--ink-faint); white-space: nowrap; }

/* ---------- page menu ---------- */
/* Every page the user may open, so a short bookmark bar never hides anything. */
.page-menu { position: relative; flex: none; display: flex; align-items: center; }
.page-menu-btn {
  display: inline-flex; align-items: center; gap: 7px;
  font: inherit; font-size: .86rem; font-weight: 500;
  padding: 7px 11px; margin-right: 10px;
  color: var(--ink); background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  cursor: pointer; white-space: nowrap;
}
.page-menu-btn:hover { border-color: var(--brand); color: var(--brand); }
.page-menu-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.page-menu-btn svg {
  width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.page-menu-btn .chev { width: 13px; height: 13px; transition: transform .15s ease; }
.page-menu-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .page-menu-btn .chev { transition: none; } }

.page-menu-list {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;
  min-width: 230px; max-height: 70vh; overflow-y: auto;
  padding: 8px; border-radius: var(--radius);
  background: var(--glass); border: 1px solid var(--glass-edge);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  backdrop-filter: blur(18px) saturate(160%);
  box-shadow: var(--shadow-lg);
}
.page-menu-list[hidden] { display: none; }
.page-menu-group {
  margin: 10px 8px 4px; font-family: var(--font-mono); font-size: .64rem;
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-faint);
}
.page-menu-group:first-child { margin-top: 2px; }
.page-menu-list a {
  display: block; padding: 8px 10px; border-radius: var(--radius-sm);
  border-bottom: none; color: var(--ink); font-size: .88rem;
}
.page-menu-list a:hover { background: var(--surface-2); color: var(--brand); }
.page-menu-list a.on { color: var(--brand); font-weight: 600; background: var(--brand-tint); }
.page-menu-foot { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); }
.page-menu-foot a { font-size: .82rem; color: var(--ink-soft); }

/* ---------- bookmark editor ---------- */
.bookmark-list { list-style: none; margin: 0 0 6px; padding: 0; }
.bookmark-list li {
  display: grid; grid-template-columns: 18px 1fr auto; gap: 10px; align-items: center;
  padding: 9px 11px; margin-bottom: 6px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); cursor: grab;
}
.bookmark-list li.dragging { opacity: .4; }
.bookmark-list li.drop-target { border-color: var(--brand); }
.bookmark-list .grip { color: var(--ink-faint); cursor: grab; }
.bookmark-list label { display: flex; align-items: center; gap: 9px; margin: 0; cursor: pointer; }
.bookmark-list .bm-label { font-size: .9rem; }
.bookmark-list .bm-group {
  font-family: var(--font-mono); font-size: .68rem; color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: .06em;
}

h1 { font-size: 1.45rem; font-weight: 600; letter-spacing: -.018em; margin: 0 0 4px; text-wrap: balance; }
h2 { font-size: 1.08rem; font-weight: 600; letter-spacing: -.01em; margin: 26px 0 10px; }
.lede { color: var(--ink-soft); font-size: .92rem; margin: 0 0 22px; max-width: 62ch; }

/* ---------- cards ---------- */
.card h2 { margin-top: 0; }
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

.tile { display: block; text-decoration: none; color: inherit; }
.tile:hover { border-color: var(--brand-bright); }
.tile .t { font-weight: 600; margin-bottom: 3px; }
.tile .d { color: var(--ink-soft); font-size: .85rem; }

/* ---------- forms ---------- */
.field > label {
  display: block; font-size: .84rem; font-weight: 500;
  color: var(--ink-soft); margin-bottom: 5px;
}
input[type=text], input[type=password], input[type=email], input[type=number],
input[type=date], input[type=time], input[type=tel], select, textarea {
  width: 100%; font: inherit; font-size: 16px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm); padding: 10px 12px; min-height: 44px;
}
textarea { min-height: 110px; resize: vertical; }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand);
}
.help { font-size: .8rem; color: var(--ink-faint); margin-top: 4px; }
.errors { color: var(--danger); font-size: .84rem; margin-top: 5px; }
.errorlist { list-style: none; padding: 0; margin: 5px 0 0; color: var(--danger); font-size: .84rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font: inherit; font-size: .92rem; font-weight: 500;
  min-height: 44px; padding: 0 16px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink);
  cursor: pointer; text-decoration: none;
}
.btn:hover { border-color: var(--brand); color: var(--brand); }
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn-primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.btn-primary:hover { background: var(--brand-bright); border-color: var(--brand-bright); color: var(--brand-ink); }
.btn[disabled] { opacity: .5; cursor: not-allowed; }

/* CASCADE REPAIR, not a design override.
 *
 * Our base `.btn` above is the quiet button, and this file loads AFTER the
 * kit's. At equal specificity the later rule wins, so our `.btn` beat every
 * one of the kit's single-class variants: `.btn-sm` stopped being small and
 * `.btn-danger` stopped being red. Nothing failed -- both classes were
 * defined, just outranked -- and the damage sat on signed-in pages that no
 * screenshot reached. `.btn-sm` had been broken since slice 2 on 47 buttons
 * when slice 6's render finally showed it.
 *
 * So each variant is re-anchored at `.btn.btn-x`, one step stronger than our
 * base, with the KIT'S OWN VALUES through the kit's tokens. The kit still owns
 * what a small or a danger button looks like; this only restores its right to
 * say so. A test fails if a kit variant is used in a template without an
 * anchor here, so the next one cannot quietly lose the same way.
 */
.btn.btn-sm { min-height: var(--control-h-sm); padding: 7px var(--space-3); font-size: var(--text-sm); }
.btn.btn-danger { background: var(--danger); border-color: var(--danger); color: var(--brand-ink); }
.btn.btn-danger:hover { background: var(--danger-hover); border-color: var(--danger-hover); color: var(--brand-ink); }
.btn.btn-ghost { background: transparent; border-color: transparent; color: var(--ink-soft); }
.btn.btn-ghost:hover { background: var(--surface); color: var(--ink); }
.actions { display: flex; gap: 9px; flex-wrap: wrap; margin-top: 18px; }

/* ---------- messages ---------- */

/* ---------- pills ---------- */

/* ---------- tables ---------- */
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
table.data { border-collapse: collapse; width: 100%; font-size: .87rem; }
table.data th, table.data td {
  text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
table.data thead th {
  background: var(--surface-2); font-size: .74rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft);
}
/* Anything the browser scrolls to -- a fragment link, a focused field pulled
   into view, a validation error -- must land clear of the pinned chrome rather
   than underneath it. Applied broadly rather than only to :target, because
   most of those jumps never set one. */
:target,
h1, h2, h3,
.card,
.field,
[id] {
  scroll-margin-top: calc(var(--chrome-h) + var(--chrome-gap));
}
html { scroll-padding-top: calc(var(--chrome-h) + var(--chrome-gap)); }
table.data thead th a { color: inherit; text-decoration: none; display: inline-flex; gap: 4px; }
table.data thead th a:hover { color: var(--brand); }
table.data tbody tr:hover { background: var(--surface-2); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data td.num, table.data td.code { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
tr.filters td { padding: 5px 8px; background: var(--surface); }
tr.filters input, tr.filters select {
  min-height: 32px; font-size: .82rem; padding: 4px 7px; min-width: 100px;
}
/* The row above a table is the kit's `.filter-bar`. What stays here is the
   one thing the kit's rule does not do: push whatever follows a `.spacer` to
   the far end, so the "Add" button sits at the right edge of the row. */
.filter-bar .spacer { margin-left: auto; }
.pagination { display: flex; gap: 6px; align-items: center; margin-top: 13px; font-size: .85rem; }

/* Column picker: checkboxes plus drag handles. */
.col-picker { display: none; margin-bottom: 12px; }
.col-picker.open { display: block; }
.col-picker ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 7px; }
.col-picker li {
  display: flex; align-items: center; gap: 7px; font-size: .84rem;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 6px 10px; background: var(--surface); cursor: grab;
}
.col-picker li.dragging { opacity: .45; }
.col-picker li .grip { color: var(--ink-faint); cursor: grab; }

/* The kit's own rules for these are what draw them now. Ours were duplicates
   with different numbers -- see the audit at the top of this file. Removed in
   slice 2, 2026-09-21: .alert and its four variants, .card, .field, .small,
   .muted, .empty, .btn-sm, .modal-actions, .modal-backdrop, .pill and its
   three variants. Variants of ours that the kit has no answer for (.card
   modifiers, .field label, the media queries) stayed. */
/* ---------- glass modal ---------- */
/* One dialog serves the whole application. Native alert/confirm/prompt are not
   used anywhere in this project: they cannot be themed, they block the page,
   and they read as though they came from somewhere other than the portal. */
/* Centred on the viewport.
   This only holds while the dialog is a child of <body>. backdrop-filter --
   which the masthead, nav and footer all carry -- establishes a containing
   block for fixed-position descendants, so a dialog nested inside one of those
   bars centres on the bar instead of the screen. Keep dialogs at body level;
   modal.js appends its own there for the same reason. */
.modal-backdrop.open { display: flex; }
.modal-glass {
  width: 100%; max-width: 460px; max-height: 86vh; overflow-y: auto;
  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; color: var(--ink);
}
.modal-glass.wide { max-width: 720px; }
.modal-glass h2 { margin: 0 0 6px; font-size: 1.05rem; }
.modal-glass .modal-body { margin: 0 0 16px; color: var(--ink-soft); font-size: .89rem; }
.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(.985); }
    to { opacity: 1; transform: none; }
  }
}

/* ---------- policy prose ---------- */
/* The box is the kit's `.terms-box`. Two things it does not do, kept here:
 *   - its height is 260px, a porthole for a 4,500-character privacy policy
 *     that a member of the public must read before consenting. 52vh grows
 *     with the screen, so the reader scrolls a page, not a slot;
 *   - it styles no headings or lists, and a policy is Markdown the staff
 *     write, rendered into here -- the rules below dress what arrives. */
.terms-box.policy { max-height: 52vh; }
.policy h1, .policy h2, .policy h3 { margin: 18px 0 8px; font-size: 1rem; }
.policy h1:first-child, .policy h2:first-child { margin-top: 0; }
.policy p, .policy li { color: var(--ink-soft); }
.policy ul, .policy ol { padding-left: 22px; }

/* ---------- stock meters ---------- */
.meter { height: 7px; border-radius: 4px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; }
.meter i { display: block; height: 100%; background: var(--brand-bright); }
/* Modifiers, renamed 2026-09-21. They used to be `.low` and `.empty` -- and
   `.empty` is also a component name in the shared kit, meaning an empty-state
   block with 40px of padding and centred text. A 7px stock bar carrying that
   class became an 87px grey slab whenever a voucher group ran out. The bug
   predates the kit (our own `.empty` did the same with 34px), which is why
   nobody traced it: it only appears when stock reaches zero. Prefixed names
   cannot collide with a component. */
.meter.meter-low i { background: var(--warn); }
.meter.meter-out i { background: var(--danger); }
/* One grid for every row, so the three columns line up down the card.
   `max-content` on the last column makes it as wide as the longest pair of
   numbers in the whole list -- which is what stops each meter starting in a
   different place. The rows are `display: contents` so their cells become
   cells of this grid rather than of a grid of their own. */
.stock-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) max-content;
}
/* The row is a box again, and `subgrid` is what lets it be one while still
   using the list's column tracks -- so the columns stay aligned down the card
   and the bar does not move a pixel.

   It was `display: contents`, which generates no box at all, so the separator
   had to be drawn on the three cells instead. Centred cells of unequal height
   put their bottom edges at unequal heights: the rule ran along, stepped up
   9px across the bar, and dropped back afterwards. A border belongs on the
   row, because the row is the thing being separated. */
.stock-row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
/* Column spacing as padding rather than `gap`, deliberately: a gap would
   resize the tracks and shift the bar, and the bar is already where it
   should be. */
.stock-row > :not(:last-child) { padding-right: 16px; }
.stock-row:last-child { border-bottom: none; }

/* Without subgrid the row cannot borrow the list's tracks, so it defines its
   own -- which is exactly the original per-row layout: aligned within a row,
   and only possibly out by a few pixels between rows. Worse than the version
   above, better than a row whose cells have nowhere to go. */
@supports not (grid-template-columns: subgrid) {
  .stock-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) max-content;
  }
}
/* The bar sits *inside* a cell rather than being one.
   Two things went wrong when it was the cell itself. `box-sizing: border-box`
   is set globally, so the cell padding ate the bar's 7px height and it
   rendered as an empty groove. And a 7px-tall rounded box with its own border
   is not something a row rule can be drawn along, so the line broke wherever a
   bar was. The wrapper takes the padding and the rule; the bar keeps exactly
   the styling it always had. */
.meter-cell { display: block; }
.stock-row .n {
  font-family: var(--font-mono); font-size: .82rem; color: var(--ink-soft);
  font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap;
}

/* ---------- misc ---------- */
.mono { font-family: var(--font-mono); }
/* The issued code is the kit's `.secret`. One deliberate override: the kit
   sets the code at 1.1rem, sized for a generated password someone copies.
   A voucher code is READ ALOUD across a desk and typed by a visitor into a
   captive portal on their phone, so it keeps the size it always had here.
   Centred, because it is the only thing on the card that matters. */
.secret.secret-issued { justify-content: center; margin: 8px 0; }
.secret.secret-issued code { font-size: 1.6rem; font-weight: 600; letter-spacing: .1em; }
.perm-group { border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 12px; margin-bottom: 11px; }
.perm-group h3 { margin: 0 0 8px; font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft); }
/* Each permission is the kit's `.check-row`. The group box stays ours: the
   kit's `.grid-matrix` is a roles-by-permissions TABLE, a different shape from
   one person's grants grouped by area, so it is not the same component. */
.perm-group .check-row { align-items: flex-start; font-size: var(--text-sm); }
.perm-group .check-row.off { opacity: .45; }
.countdown { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* The wordmark is longer than the header has room for on a narrow phone, so it
   truncates rather than pushing the sign-out button off screen. The brand mark
   beside it keeps the portal identifiable when the text is clipped. */
.app-header .wordmark {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 46vw;
}
@media (min-width: 620px) { .app-header .wordmark { max-width: none; } }

/* ---------- notification centre ---------- */
/* A popover hanging off the bell, top right -- not a centred dialog over a
   dimmed page. That treatment is reserved for confirmations, which must not be
   missed or dismissed by accident. A notification is something to glance at,
   so it neither dims nor blurs what is behind it. */
.notif-wrap { position: relative; flex: none; display: flex; align-items: center; }

/* The button itself is the kit's `.icon-btn .icon-btn-ghost`: a 38px circle
   with a hairline border, the same control every other Sanrad portal uses for
   an icon action. What stays here is only what the kit has no opinion about --
   the badge needs a positioned parent, and the icon wants a size. */
.bell { position: relative; }
.bell svg {
  width: 19px; height: 19px; fill: none;
  stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The count on the bell is the kit's `.badge` -- this is the one portal where
   that name means what the kit means by it, so we inherit rather than
   override. Two additions the kit has no opinion about:
     * a ring in the surface colour, so the badge reads as sitting ON the bell
       rather than beside it when it overlaps the icon's edge;
     * tabular numerals, so 8 -> 9 -> 10 does not shift the glyphs about.
   `.is-zero` is ours: the count element stays in the DOM for the script to
   update, and hides itself when there is nothing to report. */
.badge {
  border: 2px solid var(--surface);
  font-variant-numeric: tabular-nums;
}
.badge.is-zero { display: none; }

.bell-panel {
  position: absolute; top: calc(100% + 10px); right: 0; z-index: 45;
  width: 370px; max-width: calc(100vw - 24px);
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.bell-panel[hidden] { display: none; }
/* A small pointer back to the bell, so the popover reads as belonging to it
   rather than floating loose over the page. */
.bell-panel::before {
  content: ""; position: absolute; top: -7px; right: 13px;
  width: 12px; height: 12px; transform: rotate(45deg);
  background: var(--surface);
  border-left: 1px solid var(--line-strong);
  border-top: 1px solid var(--line-strong);
}
@media (prefers-reduced-motion: no-preference) {
  .bell-panel { animation: notif-in .13s ease-out; }
  @keyframes notif-in {
    from { opacity: 0; transform: translateY(-5px); }
    to { opacity: 1; transform: none; }
  }
}

.bell-panel-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.bell-panel-head h2 { margin: 0; flex: 1; font-size: .98rem; }

.notif-list { list-style: none; margin: 0 0 12px; padding: 0; max-height: 52vh; overflow-y: auto; }
.notif {
  display: grid; grid-template-columns: 10px 1fr; gap: 10px;
  padding: 10px 4px; border-bottom: 1px solid var(--line);
}
.notif:last-child { border-bottom: none; }
.notif-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; background: var(--ink-faint); }
.notif-success .notif-dot { background: var(--ok); }
.notif-info    .notif-dot { background: var(--brand); }
.notif-warning .notif-dot { background: var(--warn); }
.notif-error   .notif-dot { background: var(--danger); }
.notif-body p { margin: 0 0 3px; font-size: .87rem; color: var(--ink); }
.notif-body time {
  font-family: var(--font-mono); font-size: .71rem; color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
/* Unread carries weight as well as colour, so the distinction survives being
   printed, screenshotted, or read by someone who cannot separate the hues. */
.notif.is-unread .notif-body p { font-weight: 600; }
.notif.is-unread { background: var(--surface-2); border-radius: var(--radius-sm); }
.notif-empty { padding: 22px 4px; text-align: center; color: var(--ink-faint); font-size: .86rem; }

.notif-actions { display: flex; gap: 8px; }
.notif-actions form { flex: 1; }
.notif-actions .btn { width: 100%; }

/* ---------- footer ---------- */
/* Fixed to the viewport, not to the end of the document: the strip stays
   visible however far a page scrolls. Opaque rather than glass so content
   passing beneath it never shows through and muddies the text.

   z-index sits below the modal layer (200) so a dialog covers it, and below
   the sticky header (30) which it never meets anyway. */
.app-footer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 25;
  padding: 13px 16px calc(13px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--surface);
  text-align: center;
}
.app-footer p {
  margin: 0; max-width: none;
  font-size: .78rem; line-height: 1.35; color: var(--ink-faint);
}

/* On a narrow phone the line wraps to two rows, so the strip is taller and the
   clearance above must grow with it. Measured, not guessed: the text is ~53
   characters and wraps below roughly 420px. */
@media (max-width: 420px) {
  :root { --footer-h: 62px; }
}


/* ---------- range sliders ---------- */
/* The generic input rule above gives every control a 44px box and a border,
   which turns a range input into a bordered strip with a track floating in it.
   These are for judgement calls you make by eye, so they get a real track. */
input[type=range] {
  -webkit-appearance: none; appearance: none;
  width: 100%; min-height: 0; height: 6px; padding: 0;
  margin: 14px 0; border: none; border-radius: 3px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--brand); border: 2px solid var(--surface);
  box-shadow: var(--shadow); cursor: grab;
}
input[type=range]::-moz-range-thumb {
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--brand); border: 2px solid var(--surface);
  box-shadow: var(--shadow); cursor: grab; border-color: var(--surface);
}
input[type=range]:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
.field > label output {
  font-weight: 600; color: var(--brand); font-variant-numeric: tabular-nums;
}

/* ---------- collapsible help ---------- */
/* Used on the 2FA enrolment screen for the optional icon instructions. Folded
   away by default: it is cosmetic, and the screen's real job is getting a code
   scanned. */
.icon-help {
  margin: 16px 0 4px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.icon-help > summary {
  cursor: pointer; font-size: .86rem; font-weight: 500; color: var(--brand);
  list-style: none;
}
.icon-help > summary::-webkit-details-marker { display: none; }
.icon-help > summary::before { content: "+ "; font-family: var(--font-mono); }
.icon-help[open] > summary::before { content: "− "; }
.icon-help > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.icon-help p, .icon-help ul { margin: 10px 0; }
.icon-help ul { padding-left: 20px; }
.icon-help li { margin-bottom: 4px; }


/* An amount and its unit read as one control, so they sit on one row with the
   unit only as wide as it needs to be. */
.split { display: flex; gap: 8px; align-items: stretch; }
.split > :first-child { flex: 1; min-width: 0; }
.split > select { flex: 0 0 auto; width: auto; min-width: 88px; }

/* Terminal scan target: the kit's .scan-target since slice 9 (2026-09-21),
   with its is-scanning / is-error states. Only the spacing around it is
   ours, because the kit's box has no margin and sits inside our card. There
   is no field here by design -- see the template. */
.scan-target { margin: 10px 0 4px; }


/* ==========================================================================
   Event Builder — authoring a rule's wording
   ========================================================================== */

/* ---------- Step strip ---------- */
.steps {
  display: flex; flex-wrap: wrap; gap: 6px;
  list-style: none; margin: 0 0 18px; padding: 0;
}
.step { display: flex; align-items: center; }
.step > a, .step > span {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 14px 7px 9px; border: 1px solid var(--line);
  border-radius: 999px; background: var(--surface);
  font-size: .82rem; color: var(--ink-soft); text-decoration: none;
}
.step > a:hover { border-color: var(--brand); color: var(--brand); }
.step-dot {
  display: grid; place-items: center;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--surface-2); color: var(--ink-faint);
  font-size: .72rem; font-weight: 600;
}
.step-done > a > .step-dot, .step-done > span > .step-dot {
  background: var(--ok); color: #fff;
}
.step-current > a, .step-current > span {
  border-color: var(--brand); color: var(--brand); font-weight: 500;
}
.step-current .step-dot { background: var(--brand); color: var(--brand-ink); }

/* ---------- Composer layout ----------
   Desktop-first, deliberately: the Event Builder is an administrator's screen
   and is used at a desk. Only self-registration and the approval pages are
   built mobile-first, because those are the two a visitor opens on a phone.
   The narrow rule at the bottom exists so a dragged-narrow window still works,
   not because these screens are designed for one.

   The variable palette belongs beside the box you are typing into. Below it,
   you scroll away from the text to reach the tool, and then type the variable
   name from memory instead of clicking it. */
.compose {
  display: grid; gap: 20px; align-items: start;
  grid-template-columns: minmax(0, 1fr) 300px;
}
.compose-3 { grid-template-columns: minmax(0, 1fr) 300px 380px; }
@media (min-width: 1700px) {
  .compose { grid-template-columns: minmax(0, 1fr) 340px; }
  .compose-3 { grid-template-columns: minmax(0, 1fr) 340px 440px; }
}
@media (max-width: 1100px) {
  .compose, .compose-3 { grid-template-columns: minmax(0, 1fr); }
}
.compose-main { margin: 0; }
.compose-main textarea { width: 100%; }

/* Two columns of fields inside one card. Same reasoning: at a desk there is
   room to show a whole form at once instead of a single tall stack. */
.field-columns { display: grid; gap: 0 28px; grid-template-columns: 1fr 1fr; }
@media (max-width: 1100px) { .field-columns { grid-template-columns: 1fr; } }
.field-columns > section { min-width: 0; }
.field-columns h2:first-child { margin-top: 0; }

/* A reference column beside a form: what the thing you are filling in will be
   read for, and what the last one read. Same sticky behaviour as the variable
   palette, because it is used the same way — glanced at while typing. */
.side-panel {
  position: sticky; top: calc(var(--chrome-h) + 12px);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: 16px;
  max-height: calc(100vh - var(--chrome-h) - var(--footer-h) - 32px);
  overflow-y: auto;
}
.side-panel h3 {
  margin: 0 0 6px; font-size: .78rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-soft);
}
.fact-list {
  margin: 10px 0 0; padding-left: 18px;
  font-size: .82rem; color: var(--ink-soft); line-height: 1.7;
}
.facts {
  display: grid; grid-template-columns: auto 1fr; gap: 4px 12px;
  margin: 10px 0 0; font-size: .82rem;
}
.facts dt { color: var(--ink-faint); }
.facts dd { margin: 0; word-break: break-word; }
.text-warn { color: var(--warn); }
.curl-source {
  margin: 8px 0 0; padding: 10px; border-radius: var(--radius-sm);
  background: var(--surface-2); border: 1px solid var(--line);
  font-family: var(--font-mono); font-size: .7rem; line-height: 1.5;
  white-space: pre-wrap; word-break: break-all;
  max-height: 260px; overflow-y: auto;
}

/* ---------- Variable palette ---------- */
.var-panel {
  position: sticky; top: calc(var(--chrome-h) + 12px);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); padding: 14px;
  max-height: calc(100vh - var(--chrome-h) - var(--footer-h) - 32px);
  display: flex; flex-direction: column;
}
.var-panel h3 {
  margin: 0 0 2px; font-size: .78rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-soft);
}
.var-list {
  display: flex; flex-direction: column; gap: 3px;
  margin-top: 10px; overflow-y: auto;
}
.var-chip {
  display: flex; flex-direction: column; gap: 1px; text-align: left;
  padding: 6px 8px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; cursor: pointer; font: inherit; color: var(--ink);
}
.var-chip:hover { background: var(--brand-tint); border-color: var(--brand); }
.var-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.var-chip .mono { font-size: .78rem; color: var(--brand); }
.var-desc { font-size: .72rem; color: var(--ink-faint); line-height: 1.3; }

/* ---------- Call-to-action rows ---------- */
.cta-row {
  display: grid; gap: 8px; margin-bottom: 8px; align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 720px) {
  .cta-row { grid-template-columns: 175px 1fr 1.5fr auto; }
}
.cta-row input, .cta-row select { margin: 0; }
/* Spans the whole row: it explains the row above it, not one field in it. */
.btn-hint {
  grid-column: 1 / -1; font-size: .74rem; color: var(--ink-faint);
  margin: -2px 0 4px; min-height: 1em; word-break: break-all;
}
.btn-hint:empty { display: none; }
.btn-hint .mono { color: var(--ink-soft); }
.btn-hint.is-bad { color: var(--warn); }

/* A form that sits inside a table cell and is only a control or two. */
.inline-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
.inline-form select { margin: 0; min-width: 150px; }

/* ---------- Breadcrumbs ----------
   A way back up that does not depend on browser history. The arrow is the
   control; the trail is the explanation of where it goes. */
.crumbs {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 6px 14px; margin: 0 0 18px;
}
.crumb-back {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 5px 11px 5px 9px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink); text-decoration: none;
  font-size: .84rem; font-weight: 600; white-space: nowrap;
}
.crumb-back:hover { border-color: var(--accent); color: var(--accent); }
.crumb-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.crumb-trail {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px;
  list-style: none; margin: 0; padding: 0;
  font-size: .8rem; color: var(--ink-faint); min-width: 0;
}
.crumb-trail li { display: flex; align-items: baseline; gap: 4px; }
.crumb-trail li + li::before { content: "/"; color: var(--line); }
.crumb-trail a { color: var(--ink-soft); text-decoration: none; }
.crumb-trail a:hover { color: var(--accent); text-decoration: underline; }
.crumb-trail [aria-current="page"] { color: var(--ink-faint); }

/* On a phone the trail wraps to three lines and stops being scannable, so
   only the control survives. */
@media (max-width: 700px) {
  .crumb-trail { display: none; }
}

/* ---------- Downloads on the help page ----------
   Each row is a target somebody taps on a phone while holding a laptop that
   will not connect, so the hit area is the whole row, not the words. */
.downloads { margin-bottom: 22px; }
.downloads h2 { margin: 0 0 6px; font-size: 1.02rem; }
.download-list { list-style: none; margin: 12px 0 0; padding: 0;
                 display: grid; gap: 8px; }
.download-list > li { margin: 0; }
.download-list a {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; padding: 11px 14px; text-decoration: none;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2); color: var(--ink);
}
.download-list a:hover { border-color: var(--accent); }
.download-list a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.dl-label { font-weight: 600; }
.dl-meta { font-size: .78rem; color: var(--ink-soft); white-space: nowrap; }
.download-list .small { margin: 4px 2px 0; }

/* ---------- Uploaded help notes ----------
   A document, not a form: wider leading, a real heading scale, and code that
   is readable on a phone in a corridor -- which is where somebody reads this,
   holding the laptop that will not connect. */
.doc { max-width: 68ch; }
.doc h1, .doc h2, .doc h3 { text-wrap: balance; }
.doc h2 {
  margin: 34px 0 10px; padding-top: 18px; font-size: 1.22rem;
  border-top: 1px solid var(--line);
}
.doc h2:first-child { border-top: 0; padding-top: 0; margin-top: 0; }
.doc h3 { margin: 24px 0 8px; font-size: 1.02rem; }
.doc p, .doc li { line-height: 1.65; }
.doc ul, .doc ol { padding-left: 22px; margin: 10px 0; }
.doc li { margin: 5px 0; }
.doc li > ul, .doc li > ol { margin: 4px 0; }
.doc code {
  font-size: .86em; padding: .12em .34em; border-radius: 4px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.doc pre {
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 13px 15px; overflow-x: auto;
}
.doc pre code { background: none; border: 0; padding: 0; font-size: .84rem; }
.doc blockquote {
  margin: 16px 0; padding: 10px 16px; border-left: 3px solid var(--accent);
  background: var(--surface-2); border-radius: 0 var(--radius) var(--radius) 0;
}
.doc blockquote p:last-child { margin-bottom: 0; }
.doc table { width: 100%; border-collapse: collapse; margin: 14px 0; }
.doc th, .doc td {
  text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line);
  font-size: .9rem; vertical-align: top;
}
.doc th { color: var(--ink-soft); font-weight: 600; }
/* Tables in these notes are usually settings lists; let them scroll rather
   than squeeze the page sideways on a phone. */
.doc > table { display: block; overflow-x: auto; }
.doc hr { border: 0; border-top: 1px solid var(--line); margin: 26px 0; }

.doc-contents { padding: 14px 18px; margin-bottom: 22px; }
.doc-contents h2 {
  margin: 0 0 8px; font-size: .78rem; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-soft);
}
.doc-contents ul { list-style: none; margin: 0; padding: 0; }
.doc-contents li { font-size: .88rem; margin: 3px 0; }
.doc-contents .lvl-2 { padding-left: 12px; }
.doc-contents .lvl-3 { padding-left: 26px; color: var(--ink-soft); }

/* ---------- Radio choices ----------
   A list rather than inline labels: each option here carries a consequence
   worth reading, and options set side by side get skimmed. */
.choice-list { list-style: none; margin: 4px 0 0; padding: 0; }
.choice-list li { margin: 0 0 7px; }
.choice-list label { display: flex; gap: 9px; align-items: baseline;
                     font-weight: 400; cursor: pointer; }
.choice-list input[type="radio"] { margin: 0; flex: none; }
.wifi-separate { margin-top: 6px; }

/* ---------- What is sent ----------
   The slot mapping, beside the preview. It reads as a small reference table
   rather than as part of the message, because it is the wire and not the
   wording. */
.wire {
  margin-top: 18px; padding: 14px 16px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2);
}
.wire h3 { margin: 0 0 8px; font-size: .82rem; letter-spacing: .04em;
           text-transform: uppercase; color: var(--ink-soft); }
.wire-table { width: 100%; border-collapse: collapse; }
.wire-table td { padding: 3px 0; font-size: .8rem; vertical-align: baseline; }
.wire-table td:first-child { width: 40%; color: var(--ink-soft); }
.wire .small { margin: 8px 0 0; }
.wire .is-bad { color: var(--warn); }

/* ---------- Email preview ----------
   A mail client's chrome, cut down to the two parts that matter: the subject
   line and the body. The body is an iframe because it is HTML somebody typed. */
.mail-window {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); overflow: hidden;
}
.mail-subject {
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  background: var(--surface-2); font-weight: 600; font-size: .86rem;
  color: var(--ink); word-break: break-word;
}
.mail-subject.is-empty { color: var(--ink-faint); font-weight: 400; font-style: italic; }
#email-preview {
  display: block; width: 100%; height: 460px; border: 0; background: #fff;
}

/* ---------- WhatsApp preview ----------
   Deliberately WhatsApp's own colours rather than the portal's: the point of
   this panel is to answer "is this what arrives on their phone?", and a
   preview wearing portal blue answers a different question. */
/* Below the three-column breakpoint the preview wraps under the editor, where
   it would otherwise stretch to the full width of the page and stop looking
   like a phone at all. */
.preview-col { position: sticky; top: calc(var(--chrome-h) + 12px); max-width: 460px; }
.preview-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 8px;
}
.preview-head h3 {
  margin: 0; font-size: .78rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-soft);
}
.wa-phone {
  --wa-paper: #e5ddd5; --wa-bubble: #ffffff; --wa-ink: #111b21;
  --wa-soft: #667781; --wa-link: #027eb5; --wa-line: rgba(0,0,0,.08);
  background: var(--wa-paper); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px 14px;
}
:root:not([data-theme="light"]) .wa-phone,
:root[data-theme="dark"] .wa-phone {
  --wa-paper: #0b141a; --wa-bubble: #202c33; --wa-ink: #e9edef;
  --wa-soft: #8696a0; --wa-link: #53bdeb; --wa-line: rgba(255,255,255,.09);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .wa-phone {
    --wa-paper: #0b141a; --wa-bubble: #202c33; --wa-ink: #e9edef;
    --wa-soft: #8696a0; --wa-link: #53bdeb; --wa-line: rgba(255,255,255,.09);
  }
}
.wa-bubble {
  position: relative; background: var(--wa-bubble); color: var(--wa-ink);
  border-radius: 8px; border-top-left-radius: 0;
  padding: 8px 10px 6px; max-width: 100%;
  box-shadow: 0 1px 1px rgba(0,0,0,.13);
  font-size: .855rem; line-height: 1.45;
}
/* The tail. Drawn rather than imaged so it follows the bubble colour. */
.wa-bubble::before {
  content: ""; position: absolute; top: 0; left: -8px;
  border: 8px solid transparent;
  border-top-color: var(--wa-bubble); border-right: 0;
}
.wa-header { font-weight: 600; margin-bottom: 4px; }
.wa-body { white-space: pre-wrap; word-break: break-word; }
.wa-footer { margin-top: 6px; font-size: .76rem; color: var(--wa-soft); white-space: pre-wrap; }
.wa-header.is-empty, .wa-footer.is-empty { display: none; }
.wa-body.is-empty::after {
  content: "Your message appears here."; color: var(--wa-soft); font-style: italic;
}
.wa-meta {
  text-align: right; font-size: .68rem; color: var(--wa-soft);
  margin-top: 2px; user-select: none;
}
.wa-tick { color: #53bdeb; }
/* A variable, shown as a placeholder rather than pretending to be text. */
.wa-var {
  display: inline-block; padding: 0 5px; border-radius: 4px;
  background: rgba(0,168,132,.16); color: #008069;
  font-family: var(--mono, ui-monospace, monospace); font-size: .78em;
}
:root:not([data-theme="light"]) .wa-var,
:root[data-theme="dark"] .wa-var { background: rgba(0,168,132,.24); color: #7fe0c8; }
.wa-buttons { margin-top: 2px; }
.wa-cta {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  background: var(--wa-bubble); color: var(--wa-link);
  border-top: 1px solid var(--wa-line);
  padding: 9px 10px; font-size: .84rem; font-weight: 500;
}
.wa-cta:first-child { border-top: 0; margin-top: 2px; }
.wa-cta:last-child { border-radius: 0 0 8px 8px; }
.wa-cta-icon { font-size: .95em; }
.wa-body code {
  font-family: var(--font-mono); font-size: .9em;
  background: rgba(127,127,127,.16); border-radius: 3px; padding: 0 3px;
}

/* ---------- frosted strips ----------
   The three pinned strips -- masthead, navigation, footer -- are opaque where
   they are defined above. This is what makes them glass. The tint and alpha
   are fixed in :root above and the blur is the kit's; nothing is tunable any
   more (the Appearance settings were removed on 2026-09-21).

   Behind @supports because the fallback has to be a readable opaque bar. A
   browser that cannot blur would otherwise get a see-through strip with page
   text sliding underneath it, which is worse than no frost at all.

   The colour comes from --glass-rgb, which is defined per theme, so one
   alpha serves light and dark alike. The clamp in --glass-bar is kept as a
   floor on readability, should anyone change the alpha by hand. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .app-header,
  .nav,
  .app-footer {
    background: var(--glass-bar);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
    backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  }
}
