@font-face {
  font-family: "boxicons";
  font-style: normal;
  font-weight: 400;
  src:
    url("assets/ui-kit/fonts/boxicons.woff2") format("woff2"),
    url("assets/ui-kit/fonts/boxicons.woff") format("woff"),
    url("assets/ui-kit/fonts/boxicons.ttf") format("truetype");
}

:root {
  color-scheme: light;

  /* The app is a single white sheet floating on a soft grey desk. Depth comes
     from that one elevation plus generous space — not from borders on every
     box, which is what made the old build read as a control panel. */
  --page: #dcdde0;
  --bg: #ffffff;
  --surface: #ffffff;
  --surface-soft: #f4f5f7;
  --surface-raised: #ffffff;
  --ink: #10151b;
  /* Muted text appears on both the sheet and the soft grey blocks, so it has
     to clear 4.5:1 on the darker of the two: 5.19:1 on white, 4.75:1 on
     --surface-soft. */
  --muted: #656f7d;
  --line: #edeff2;
  --line-strong: #e0e3e8;

  /* The accent is a soft blue that carries NEAR-BLACK text, not white. A pale
     fill with dark ink reads as friendly and clears AA comfortably (11.9:1),
     where the same blue behind white text would fail outright. --primary-ink
     is the separate, darker variant for links and active labels. */
  --primary: #a9cbf2;
  --primary-dark: #93bcec;
  --primary-on: #10151b;
  --primary-ink: #2563c7;
  --primary-soft: #eaf2fd;
  --chat-mine-background: #eaf2fd;

  /* Status colours come in pairs: a saturated dot for the glance, a darker
     ink for the word beside it, because the dot hue alone never passes AA. */
  --success: #16a34a;
  --success-ink: #15803d;
  --warning: #f59e0b;
  --warning-ink: #a35a06;
  --danger: #ef4444;
  --danger-ink: #c62828;
  --teal: #0ea5b7;
  --teal-ink: #0f766e;

  --shadow: 0 1px 2px rgba(16, 21, 27, 0.04);
  --shadow-raised: 0 24px 60px rgba(16, 21, 27, 0.13);
  --radius: 14px;
  --radius-lg: 22px;
  --radius-xl: 30px;
  --radius-pill: 999px;
  /* Labelled controls — buttons, inputs, filter selects — share one small arc.
     Icon-only controls stay circles; that contrast is deliberate. */
  --radius-control: 5px;
  /* A gear, inlined so the Settings icon needs no asset file. */
  /* A mark the icon font does not carry, inlined as a mask so it takes
     currentColor like every other icon in the app. */
  --icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M16 1H4a2 2 0 0 0-2 2v14h2V3h12V1zm3 4H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H8V7h11v14z'/%3E%3C/svg%3E");
  --icon-wallet-topup: url("assets/ui-kit/icons/wallet-topup.svg");
  --icon-chat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zM7 9h10v2H7V9zm6 5H7v-2h6v2zm4-6H7V6h10v2z'/%3E%3C/svg%3E");
  --icon-cog: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19.14 12.94a7.07 7.07 0 0 0 0-1.88l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.03 7.03 0 0 0-1.62-.94l-.36-2.54a.5.5 0 0 0-.5-.42h-3.84a.5.5 0 0 0-.5.42l-.36 2.54c-.59.24-1.13.56-1.62.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 8.84a.5.5 0 0 0 .12.64l2.03 1.58a7.07 7.07 0 0 0 0 1.88l-2.03 1.58a.5.5 0 0 0-.12.64l1.92 3.32c.12.21.37.3.6.22l2.39-.96c.49.38 1.03.7 1.62.94l.36 2.54a.5.5 0 0 0 .5.42h3.84a.5.5 0 0 0 .5-.42l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.23.08.48-.01.6-.22l1.92-3.32a.5.5 0 0 0-.12-.64zM12 15.6A3.6 3.6 0 1 1 12 8.4a3.6 3.6 0 0 1 0 7.2z'/%3E%3C/svg%3E");
  /* A megaphone, inlined for the same reason as the cog above. */
  --icon-megaphone: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20 3.5a1 1 0 0 0-1.6-.8L10.9 8H6a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.6l1.5 4.6a1 1 0 0 0 .95.68h2.1a1 1 0 0 0 .93-1.37L10.9 16h.05l7.45 5.3A1 1 0 0 0 20 20.5zm-2 15.05-6.4-4.55H6a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1h5.6L18 5.45z'/%3E%3C/svg%3E");
  /*
   * In rem, not px, and deliberately.
   *
   * The labels are set in rem — this file's root is 87.5%, so 1rem is 14px at
   * a browser's default — while the column was a fixed 248px. A reader who
   * runs a larger default font size therefore got wider words in a column
   * that was not getting any wider, and the longest of them, "Announcements &
   * Events", walked out of the sidebar and across the page. Sizing the column
   * in the same unit as the words it holds keeps that fit constant at any
   * font size. 20rem is 280px at the default, which leaves the longest label
   * 16px of room rather than the 1px it had.
   */
  --sidebar: 20rem;

  --body-background: var(--page);
  --sidebar-background: transparent;
  --topbar-background: var(--surface);
  --brand-mark-background: transparent;
  --brand-mark-border: transparent;
  --theme-toggle-background: var(--surface-soft);
  --theme-toggle-hover: var(--line-strong);
  /* Idle navigation labels are the primary way around the app, so they are
     held to full text contrast (4.86:1) rather than the 3:1 allowed for
     incidental UI. */
  --nav-ink: #69737f;
  --nav-hover-bg: var(--surface-soft);
  --nav-hover-ink: var(--ink);
  --sidebar-eyebrow: #98a1ad;
  --placeholder: #838d9a;
  --mode-active-shadow: none;
  --mode-active-ink: var(--ink);
  --status-primary-ink: var(--primary-ink);
  --progress-track: #eceef1;
  --row-hover: #fafbfc;
  /* The band behind a table's header row. Opaque, not a tint of the surface:
     a sticky header scrolls over its own rows and has to hide them. */
  --table-head-background: #f4f5f7;
  --modal-backdrop: rgba(16, 21, 27, 0.42);
  /* Circular icon badges are the signature of this layout; one token keeps
     every one of them the same size. */
  --badge: 46px;

  font-size: 87.5%;
  font-family:
    Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Dark is now the alternate rather than the default, so it inverts the same
   structure instead of defining its own. The sheet becomes near-black, the
   desk behind it darker still, and the pale accent darkens just enough to
   still carry near-black text. */
:root[data-bs-theme="dark"] {
  color-scheme: dark;
  --page: #0a0d11;
  --bg: #12171e;
  --surface: #12171e;
  --surface-soft: #1a212a;
  --surface-raised: #12171e;
  --ink: #e8eef4;
  --muted: #94a1b1;
  --line: #212a34;
  --line-strong: #2c3742;

  --primary: #8ab4e8;
  --primary-dark: #9dc2ee;
  --primary-on: #0a0d11;
  --primary-ink: #9cc4f0;
  --primary-soft: rgba(138, 180, 232, 0.14);
  /* Opaque, unlike --primary-soft: a bubble is not a panel tint. */
  --chat-mine-background: #24303f;

  --success: #4ade80;
  --success-ink: #6ee7a0;
  --warning: #fbbf24;
  --warning-ink: #fcd34d;
  --danger: #f87171;
  --danger-ink: #fca5a5;
  --teal: #67e8f9;
  --teal-ink: #8ceefb;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-raised: 0 24px 60px rgba(0, 0, 0, 0.5);

  --body-background: var(--surface);
  --sidebar-background: transparent;
  --topbar-background: var(--surface);
  --brand-mark-background: transparent;
  --brand-mark-border: transparent;
  --theme-toggle-background: var(--surface-soft);
  --theme-toggle-hover: var(--line-strong);
  --nav-ink: #8d9aab;
  --nav-hover-bg: var(--surface-soft);
  --nav-hover-ink: var(--ink);
  --sidebar-eyebrow: #6b7889;
  --placeholder: #6b7889;
  --status-primary-ink: var(--primary-ink);
  --progress-track: #212a34;
  --row-hover: rgba(255, 255, 255, 0.028);
  --table-head-background: #1a212a;
  --modal-backdrop: rgba(4, 6, 9, 0.7);
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  padding: 0;
  background: var(--body-background);
  background-attachment: fixed;
  color: var(--ink);
  transition:
    background-color 180ms ease,
    color 180ms ease;
}

/* The whole application is one sheet, edge to edge with the window. It still
   drives the rest of the styling — because the sheet is the only container,
   nothing inside it needs a border to prove it is one — but it no longer
   floats: the framing margin only shrank the usable width. */
.app-shell {
  min-height: 100vh;
  border-radius: 0;
  background: var(--surface);
  box-shadow: none;
}

button,
input,
select,
textarea {
  font: inherit;
}

button,
.file-button {
  min-height: 40px;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  padding: 0.6rem 1.15rem;
  cursor: pointer;
  font-weight: 600;
  line-height: 1.1;
  white-space: nowrap;
  transition:
    background-color 160ms ease,
    border-color 160ms ease,
    color 160ms ease,
    transform 160ms ease;
}

button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.file-button:focus-within {
  outline: 3px solid color-mix(in srgb, var(--primary-ink) 34%, transparent);
  outline-offset: 2px;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

button.is-loading {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
}

button.is-loading::before,
.loading-spinner {
  content: "";
  width: 1rem;
  height: 1rem;
  flex: 0 0 auto;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 999px;
  animation: auth-spin 800ms linear infinite;
}

@keyframes auth-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Pale fill, near-black label. The reference's primary button works because
   the colour is the background and the text stays the darkest thing on it. */
.primary {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-on);
}

.primary:hover {
  background: var(--primary-dark);
  border-color: var(--primary-dark);
}

.secondary,
.file-button {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
}

.secondary:hover,
.file-button:hover {
  border-color: var(--ink);
  background: var(--surface);
  color: var(--ink);
}

.danger {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border-color: transparent;
  color: var(--danger-ink);
}

.danger:hover {
  background: color-mix(in srgb, var(--danger) 24%, transparent);
}

/* Icon-only controls are circles throughout, which is what makes the layout
   read as this design rather than a generic admin theme. */
.icon-button {
  display: inline-grid;
  place-items: center;
  width: 40px;
  min-width: 40px;
  padding: 0;
  border-radius: 50%;
}

/*
 * A dialog's close control. It used to be the literal letter "x" in a 40px
 * circle, which sat at the wrong optical weight beside a real button and drew
 * more attention than the action next to it. The glyph is now drawn — two
 * bars, so it is a true cross rather than a lowercase letter — in a square
 * that matches the arc of every other control.
 */
.icon-button.dialog-close {
  position: relative;
  width: 32px;
  min-width: 32px;
  height: 32px;
  min-height: 32px;
  border-radius: var(--radius-control);
  border: 1px solid transparent;
  background: transparent;
  color: var(--muted);
  box-shadow: none;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.icon-button.dialog-close::before,
.icon-button.dialog-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 13px;
  height: 1.5px;
  border-radius: 2px;
  background: currentColor;
}

.icon-button.dialog-close::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.icon-button.dialog-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.icon-button.dialog-close:hover,
.icon-button.dialog-close:focus-visible {
  background: var(--surface-soft);
  border-color: var(--line-strong);
  color: var(--ink);
}

.icon-button.dialog-close:active {
  background: var(--line-strong);
}

.mobile-menu-toggle,
.mobile-nav-close,
.mobile-nav-backdrop {
  display: none;
}

.mobile-menu-toggle,
.mobile-nav-close {
  flex: 0 0 42px;
  place-items: center;
  width: 42px;
  min-height: 42px;
  border-color: var(--line);
  background: var(--surface-soft);
  color: var(--ink);
}

/* The menu button now stands on a 1000px tablet as well as a phone, where
   there is a pointer to answer. It had no hover state at all, having only ever
   been touched. */
.mobile-menu-toggle:hover,
.mobile-nav-close:hover {
  border-color: var(--line-strong);
  background: var(--nav-hover-bg);
}

.mobile-menu-toggle:active,
.mobile-nav-close:active {
  background: var(--line-strong);
}

.mobile-menu-toggle span,
.mobile-nav-close span {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 999px;
  background: currentColor;
  transition:
    opacity 160ms ease,
    transform 160ms ease;
}

.mobile-nav-close {
  position: relative;
}

.mobile-nav-close span {
  position: absolute;
}

.mobile-nav-close span:first-child {
  transform: rotate(45deg);
}

.mobile-nav-close span:last-child {
  transform: rotate(-45deg);
}

body.mobile-nav-open .mobile-menu-toggle span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}

body.mobile-nav-open .mobile-menu-toggle span:nth-child(2) {
  opacity: 0;
}

body.mobile-nav-open .mobile-menu-toggle span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

/* Fixed, but inset to sit inside the sheet rather than against the viewport,
   so the sheet's rounded corner is never cut by the sidebar. */
.sidebar {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--sidebar);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2rem;
  /* Matches the workspace's top inset so the mark and the greeting start on
     the same line rather than one floating below the other. */
  padding: 1.2rem 1.1rem 1.6rem;
  background: var(--sidebar-background);
  border-right: 1px solid var(--line);
  color: var(--ink);
  transition:
    width 180ms ease,
    padding 180ms ease;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  min-height: 56px;
  padding: 0 0.5rem;
}

.theme-toggle {
  flex: 0 0 40px;
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  min-height: 40px;
  border: 1px solid transparent;
  border-radius: 50%;
  padding: 0;
  background: var(--theme-toggle-background);
  color: var(--ink);
}

.theme-toggle:hover {
  background: var(--theme-toggle-hover);
  color: var(--nav-hover-ink);
}

.theme-toggle-icon {
  display: block;
  width: 18px;
  height: 18px;
  background: currentColor;
  mask-position: center;
  mask-repeat: no-repeat;
  mask-size: contain;
  -webkit-mask-position: center;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: contain;
}

.theme-light-icon {
  mask-image: url("assets/ui-kit/icons/light.svg");
  -webkit-mask-image: url("assets/ui-kit/icons/light.svg");
}

.theme-dark-icon {
  display: none;
  mask-image: url("assets/ui-kit/icons/dark.svg");
  -webkit-mask-image: url("assets/ui-kit/icons/dark.svg");
}

:root[data-bs-theme="light"] .theme-light-icon {
  display: none;
}

:root[data-bs-theme="light"] .theme-dark-icon {
  display: block;
}

/* The mark stands on its own beside the wordmark — no plate behind it. */
.brand-mark {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: var(--brand-mark-background);
  box-shadow: none;
}

.brand-mark img {
  display: block;
  width: 30px;
  height: 30px;
}

.brand h1,
.topbar h2,
.panel h3 {
  margin: 0;
  letter-spacing: 0;
}

.brand h1 {
  font-size: 1.18rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.022em;
}

.eyebrow {
  margin: 0 0 0.3rem;
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.sidebar .eyebrow {
  color: var(--sidebar-eyebrow);
}

.nav-tabs {
  position: relative;
  display: grid;
  gap: 0.15rem;
  margin-top: 0.5rem;
}

/*
 * The brother's balance, at the head of his own column.
 *
 * A card rather than a nav row: it is a fact he reads, not a place he goes,
 * and the two controls in it do different things — the figure opens the
 * wallet, the plus adds to it.
 */
.sidebar-wallet {
  display: flex;
  align-items: stretch;
  gap: 0.4rem;
  margin: 0 0 0.6rem;
}

.sidebar-wallet-figure {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  gap: 0.1rem;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-soft);
  color: var(--ink);
  text-align: left;
  box-shadow: none;
  cursor: pointer;
  transition: border-color 160ms ease, background-color 160ms ease;
}

.sidebar-wallet-figure strong {
  font-size: 1.05rem;
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-wallet-figure .eyebrow {
  margin: 0;
}

.sidebar-wallet-figure .muted {
  font-size: 0.72rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-wallet-figure:hover {
  border-color: var(--line-strong);
  background: var(--nav-hover-bg);
}

.sidebar-wallet-figure:focus-visible,
.sidebar-wallet-topup:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

/* The purse with a plus in it, masked so it takes the button's ink rather
   than carrying a colour of its own — the same treatment the impersonate
   control uses, and the reason it stays legible when the theme flips. */
.sidebar-wallet-topup {
  position: relative;
  flex: 0 0 auto;
  width: 44px;
  min-width: 44px;
  padding: 0;
  border: 1px solid var(--primary);
  border-radius: var(--radius);
  background: var(--primary);
  color: var(--primary-on);
  box-shadow: none;
  cursor: pointer;
  transition: background-color 160ms ease, border-color 160ms ease;
}

.sidebar-wallet-topup:hover {
  background: var(--primary-dark);
  border-color: var(--primary-dark);
}

.sidebar-wallet-topup span {
  position: absolute;
  top: 50%;
  left: 50%;
  /* The purse carries fine detail — a coin, a plus, a flap — so it is drawn
     a size up from a plain glyph or it reads as a smudge. */
  width: 1.5rem;
  height: 1.5rem;
  transform: translate(-50%, -50%);
  background: currentColor;
  -webkit-mask: var(--icon-wallet-topup) center / contain no-repeat;
  mask: var(--icon-wallet-topup) center / contain no-repeat;
}

/* Compact: the figure has nowhere to go, so the card becomes the one control
   that still means something without words. */
body.sidebar-compact .sidebar-wallet-figure {
  display: none;
}

body.sidebar-compact .sidebar-wallet-topup {
  width: 100%;
}

/*
 * The branch under Settings.
 *
 * Indented to the width of the icons above it so the three names line up with
 * the labels rather than with their plates, and set a size down: they are
 * where you are going next, not another place to go.
 */
.nav-subnav {
  display: grid;
  gap: 0.1rem;
  margin: 0.15rem 0 0.35rem;
  padding-left: 2.6rem;
}

.nav-subtab {
  width: 100%;
  min-height: 34px;
  padding: 0.35rem 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--nav-ink);
  font-size: 0.92rem;
  font-weight: 500;
  text-align: left;
  white-space: nowrap;
  box-shadow: none;
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease;
}

.nav-subtab:hover {
  background: var(--nav-hover-bg);
  color: var(--nav-hover-ink);
}

.nav-subtab:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: -2px;
}

/* The current one is named by weight and a rule down its left edge rather than
   by a second filled plate: one filled thing in a column is a position, two is
   a competition. */
.nav-subtab.active {
  background: var(--nav-hover-bg);
  color: var(--nav-hover-ink);
  font-weight: 700;
  box-shadow: inset 2px 0 0 var(--ink);
}

.nav-tab-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
}

.nav-tab {
  display: flex;
  align-items: center;
  width: 100%;
  justify-content: flex-start;
  gap: 0.85rem;
  min-height: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 0.5rem 0.75rem;
  border-color: transparent;
  background: transparent;
  color: var(--nav-ink);
  font-size: 1.02rem;
  font-weight: 500;
  line-height: 1.1;
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color 160ms ease,
    color 160ms ease;
}

.dashboard-nav-row .nav-tab {
  flex: 1 1 auto;
  min-width: 0;
}

/* Only the mobile "hide menu" button lives here now; the theme and sidebar
   toggles moved into the header. It no longer needs to be pinned outside the
   sidebar padding, where it overlapped the first nav item. */
.sidebar-control-stack {
  display: grid;
  gap: 1px;
  justify-items: end;
}

.sidebar-toggle {
  flex: 0 0 40px;
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  min-height: 40px;
  border: 1px solid transparent;
  border-radius: 50%;
  padding: 0;
  background: var(--theme-toggle-background);
  color: var(--ink);
}

.sidebar-toggle:hover {
  background: var(--nav-hover-bg);
  color: var(--nav-hover-ink);
}

.sidebar-toggle-icon {
  display: block;
  width: 18px;
  height: 18px;
  background: currentColor;
  mask-position: center;
  mask-repeat: no-repeat;
  mask-size: contain;
  -webkit-mask-position: center;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: contain;
}

.sidebar-hide-icon {
  mask-image: url("assets/ui-kit/icons/hide.svg");
  -webkit-mask-image: url("assets/ui-kit/icons/hide.svg");
}

.sidebar-show-icon {
  display: none;
  mask-image: url("assets/ui-kit/icons/show.svg");
  -webkit-mask-image: url("assets/ui-kit/icons/show.svg");
}

body.sidebar-compact .sidebar-hide-icon {
  display: none;
}

body.sidebar-compact .sidebar-show-icon {
  display: block;
}

/* Each item carries its icon in a rounded plate. Idle it is invisible and the
   glyph is grey; active it fills with ink and the glyph reverses out. That
   filled plate, not a coloured bar, is what marks the current section. */
.nav-tab::before {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: 11px;
  font-family: "boxicons";
  font-size: 1.15rem;
  line-height: 1;
  transition:
    background-color 160ms ease,
    color 160ms ease;
}

.nav-tab.active::before {
  background: var(--ink);
  color: var(--surface);
}

/* The lodge's dashboard and a brother's own never appear in the same sidebar,
   so they share one mark rather than inventing a second for the same idea. */
.nav-tab[data-tab="dashboard"]::before,
.nav-tab[data-tab="memberDashboard"]::before {
  content: "\ed75";
}

.nav-tab[data-tab="superAdmin"]::before {
  content: "\ecf6";
}

.nav-tab[data-tab="members"]::before {
  content: "\edd6";
}

/*
 * Administrators uses the shipped admin-alt.svg (a person with a gear) rather
 * than a boxicons glyph: the bundled font has no icon that reads as
 * "administrator". Masking keeps it on currentColor, so it inherits the same
 * idle, hover and active colours as the glyph icons. It is now the only user
 * of this asset — signing in as a member has its own, because the two actions
 * looked identical while they shared one.
 */
.nav-tab[data-tab="platformAdmins"]::before {
  content: "";
  background: currentColor;
  -webkit-mask: url("assets/ui-kit/icons/admin-alt.svg") center / 1.15rem no-repeat;
  mask: url("assets/ui-kit/icons/admin-alt.svg") center / 1.15rem no-repeat;
}

/*
 * The masked icon paints with `background`, so the active plate cannot be a
 * background — and it cannot be an inset box-shadow either: the mask clips
 * everything the element paints, shadow included, so that attempt produced a
 * dark glyph on no plate at all.
 *
 * The plate therefore needs a layer the mask does not touch. ::after supplies
 * one, sat behind the glyph.
 */
.nav-tab[data-tab="platformAdmins"] {
  position: relative;
}

.nav-tab[data-tab="platformAdmins"]::before {
  position: relative;
  z-index: 1;
}

.nav-tab[data-tab="platformAdmins"].active::before {
  color: var(--surface);
  box-shadow: none;
}

.nav-tab[data-tab="platformAdmins"].active::after {
  content: "";
  position: absolute;
  /* Matches .nav-tab's 0.75rem inline padding and the 34px icon box. */
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--ink);
  z-index: 0;
}

.nav-tab[data-tab="attendance"]::before {
  content: "\ed00";
}

.nav-tab[data-tab="dues"]::before {
  content: "\eefa";
}

.nav-tab[data-tab="expenses"]::before {
  content: "\ee89";
}

/* The wallets get the purse rather than the ledger the dues use: what the
   lodge HOLDS for its brethren is a different thing from what it has billed
   them. */
.nav-tab[data-tab="wallets"]::before {
  content: "\ec20";
}

.nav-tab[data-tab="portal"]::before {
  content: "\edef";
}

/* The brother's other three sections. Meetings borrows the calendar the
   attendance register uses and Dues the wallet; the megaphone is masked from
   an inline path, because the bundled boxicons build names its glyphs only as
   uniXXXX and picking one by codepoint is guesswork. */
.nav-tab[data-tab="memberAnnouncements"]::before,
.nav-tab[data-tab="notices"]::before {
  content: "";
  background: currentColor;
  -webkit-mask: var(--icon-megaphone) center / 1.15rem no-repeat;
  mask: var(--icon-megaphone) center / 1.15rem no-repeat;
  position: relative;
  z-index: 1;
}

.nav-tab[data-tab="memberAnnouncements"],
.nav-tab[data-tab="notices"] {
  position: relative;
}

.nav-tab[data-tab="memberAnnouncements"].active::before,
.nav-tab[data-tab="notices"].active::before {
  color: var(--surface);
  box-shadow: none;
}

.nav-tab[data-tab="memberAnnouncements"].active::after,
.nav-tab[data-tab="notices"].active::after {
  content: "";
  position: absolute;
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--ink);
  z-index: 0;
}

.nav-tab[data-tab="memberAnnouncements"].active:hover::before,
.nav-tab[data-tab="memberAnnouncements"].active:focus-visible::before,
.nav-tab[data-tab="notices"].active:hover::before,
.nav-tab[data-tab="notices"].active:focus-visible::before {
  color: var(--surface);
}

.nav-tab[data-tab="memberEvents"]::before {
  content: "\ed00";
}

.nav-tab[data-tab="memberDues"]::before {
  content: "\eefa";
}

/* The brother's wallet takes the same purse the lodge's wallets section uses,
   so the two sides of one idea look like one idea. */
.nav-tab[data-tab="memberWallet"]::before {
  content: "\ec20";
}

.nav-tab[data-tab="reports"]::before {
  content: "\ee8d";
}

/* Masked, like the cog and the megaphone, for the same reason. */
.nav-tab[data-tab="messages"]::before {
  content: "";
  background: currentColor;
  -webkit-mask: var(--icon-chat) center / 1.15rem no-repeat;
  mask: var(--icon-chat) center / 1.15rem no-repeat;
  position: relative;
  z-index: 1;
}

.nav-tab[data-tab="messages"] {
  position: relative;
}

.nav-tab[data-tab="messages"].active::before {
  color: var(--surface);
  box-shadow: none;
}

/* The dark plate behind an active masked icon, which the font-glyph items get
   from their own box-shadow. */
.nav-tab[data-tab="messages"].active::after {
  content: "";
  position: absolute;
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--ink);
  z-index: 0;
}

/*
 * Masked rather than a font glyph, for the same reason as Administrators: the
 * bundled boxicons build exposes its glyphs only as uniXXXX, so picking a cog
 * out of it is guesswork. Inlining the path keeps the icon on currentColor and
 * adds no asset to deploy.
 */
.nav-tab[data-tab="settings"]::before {
  content: "";
  background: currentColor;
  -webkit-mask: var(--icon-cog) center / 1.15rem no-repeat;
  mask: var(--icon-cog) center / 1.15rem no-repeat;
  position: relative;
  z-index: 1;
}

.nav-tab[data-tab="settings"] {
  position: relative;
}

.nav-tab[data-tab="settings"].active::before {
  color: var(--surface);
  box-shadow: none;
}

.nav-tab[data-tab="settings"].active::after {
  content: "";
  position: absolute;
  left: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--ink);
  z-index: 0;
}

.nav-tab[data-tab="settings"].active:hover::before,
.nav-tab[data-tab="settings"].active:focus-visible::before {
  color: var(--surface);
}

.nav-tab:hover {
  background: var(--nav-hover-bg);
  color: var(--nav-hover-ink);
}

.nav-tab:hover::before {
  color: var(--nav-hover-ink);
}

/* The active item's icon is reversed out of an ink-filled plate. Plain :hover
   repaints the glyph in --nav-hover-ink, which IS --ink — so hovering the
   current section made its icon vanish into its own plate. The active state
   has to restate the reversal at a specificity that beats :hover. */
.nav-tab.active:hover::before,
.nav-tab.active:focus-visible::before {
  color: var(--surface);
}

/* Same trap for the masked Administrators icon, which paints with
   background: currentColor rather than as a glyph. */
.nav-tab[data-tab="platformAdmins"].active:hover::before,
.nav-tab[data-tab="platformAdmins"].active:focus-visible::before {
  color: var(--surface);
}

.nav-tab.active {
  position: relative;
  background: transparent;
  color: var(--ink);
  font-weight: 700;
}

/*
 * While the column is being measured, every entry is weighed as though it were
 * the current one — because any of them can be. The current entry is bold, and
 * a column fitted to the lighter weight fits until you arrive at the longest
 * section, at which point that entry gains a dozen pixels and pushes the whole
 * list out over the page.
 */
.nav-tabs.is-measuring .nav-tab,
.nav-tabs.is-measuring .nav-subtab {
  font-weight: 700;
}

/* The top inset is deliberately smaller than the sides. A 2rem band above the
   greeting cost a line of real content on every screen and bought nothing —
   the header already separates itself with a rule. The narrow breakpoints
   below have always used ~1.15rem here; this brings the desktop into line. */
.workspace {
  margin-left: var(--sidebar);
  min-width: 0;
  padding: 1.2rem 2.4rem 3rem;
  transition: margin-left 180ms ease;
}

/* The header is page furniture, not a card. It carries no rule of its own —
   the stat strip below it supplies the only horizontal line up here. */
/*
 * The sticky header has to be an opaque band the full width of the workspace,
 * not just of its own text. It used to paint only the content column and only
 * as far as its padding reached, so a scrolling page slid up through the gutters
 * beside it and through the transparent gap beneath — headings appeared sliced
 * in half against white. The negative margins pull it out to the workspace's
 * own padding; the border gives the content something to disappear under.
 */
.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: grid;
  gap: 0.75rem;
  margin: -1.2rem -2.4rem 1.5rem;
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  border-radius: 0;
  padding: 1.2rem 2.4rem 1rem;
  background: var(--topbar-background);
  box-shadow: none;
}

.topbar-main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-width: 0;
}

/* With the sidebar toggle now first in the header row, the title must claim the
   remaining space or space-between centres it. */
.topbar-title {
  min-width: 0;
  margin-right: auto;
}

/* The greeting leads the page but stays a heading, not a billboard. The
   reference's display size works there because that page carries three
   figures; here it towered over tables of real data. */
.topbar h2 {
  font-size: clamp(1.35rem, 1.9vw, 1.6rem);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.022em;
}

.topbar-subtitle {
  margin: 0.25rem 0 0;
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.45;
}

/* Date, then a circular icon button — the top-right cluster of the reference. */
.topbar-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.75rem;
  color: var(--muted);
  font-size: 0.95rem;
  font-weight: 500;
}

.topbar-meta > * {
  white-space: nowrap;
}

.topbar-meta > * + *::before {
  content: "·";
  margin-right: 0.75rem;
  color: var(--line-strong);
}

.topbar-meta .icon-button {
  background: var(--surface-soft);
  border-color: transparent;
  color: var(--ink);
}

.topbar-actions,
.auth-actions,
.toolbar,
.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

.member-toolbar {
  display: grid;
  grid-template-columns: max-content minmax(180px, 1.2fr) minmax(150px, 0.8fr) minmax(140px, 0.7fr);
  flex: 1 1 720px;
  align-items: center;
  justify-content: end;
}

.member-toolbar .search-input,
.member-toolbar select {
  width: 100%;
}

.auth-actions {
  justify-content: flex-end;
}

.impersonation-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.65rem;
  border: 1px solid var(--primary);
  border-radius: 7px;
  padding: 0.42rem 0.55rem;
  background: color-mix(in srgb, var(--primary) 12%, var(--surface));
  box-shadow: none;
}

.impersonation-banner > div {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.55rem;
  min-width: 0;
}

.impersonation-banner .eyebrow {
  margin: 0;
  font-size: 0.64rem;
}

.impersonation-banner strong,
.impersonation-banner span {
  display: block;
}

.impersonation-banner strong {
  font-size: 0.9rem;
  line-height: 1.15;
}

.impersonation-banner span {
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 700;
}

.impersonation-banner button {
  min-height: 30px;
  padding: 0.34rem 0.55rem;
  font-size: 0.78rem;
}

.tab-panel {
  display: none;
}

.tab-panel.active {
  display: grid;
  gap: 1rem;
}

.auth-gate-panel {
  min-height: min(70vh, 640px);
  align-content: center;
  justify-items: center;
}

/* Panels lost their edges when the sheet became the only container, but the
   sign-in form is the one place that still needs to read as an object: with
   nothing around it, three fields adrift on a white page look like a page
   that failed to load. So this one keeps a fill and a radius. */
/* .panel resets padding to 0 further down the file; this needs both classes to
   outrank it rather than relying on source order. */
.panel.auth-gate-card {
  width: 100%;
  max-width: 440px;
  border-radius: var(--radius-xl);
  padding: 2.4rem 2.2rem 2.6rem;
  background: var(--surface-soft);
}

/* The sign-in card is the only thing on the page, so its heading is centred
   over the fields rather than flush left the way a panel's is. */
.auth-gate-card .panel-header {
  justify-content: center;
  margin-bottom: 0.5rem;
  text-align: center;
}

/* An entry in one of the portal's lists is a control now: it opens the whole
   of what was posted. It keeps the flat look of the row it replaced — a list
   of buttons that looked like buttons would read as a toolbar — and answers
   the pointer and the keyboard instead. */
.notice-item {
  width: 100%;
  align-items: flex-start;
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  padding: 0.55rem 0.4rem 0.55rem;
  background: transparent;
  box-shadow: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.notice-item:hover {
  background: var(--nav-hover-bg);
}

.notice-item:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: -2px;
}

.notice-item:active {
  background: var(--surface-soft);
}

.notice-item-main {
  display: grid;
  gap: 0.15rem;
  min-width: 0;
}

.notice-item-main strong {
  font-size: 0.88rem;
}

/* One line of it, however long it was — the rest is behind the click. */
.notice-item-main .muted {
  font-size: 0.8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.notice-item-aside {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 0.78rem;
  white-space: nowrap;
}

.notice-card {
  width: min(100%, 560px);
}

/*
 * A notice inside a table row, opening the same dialog the portal opens.
 *
 * A table cell that truncates the thing a reader wants is only useful if the
 * rest is one click away, so the whole cell is the control. It is deliberately
 * not painted like a button: a row of buttons down a table reads as a row of
 * actions, and this is a row of notices.
 */
.row-notice {
  display: grid;
  gap: 0.15rem;
  width: 100%;
  margin: -0.3rem -0.4rem;
  padding: 0.3rem 0.4rem;
  border: 0;
  border-radius: 0.4rem;
  background: transparent;
  box-shadow: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.row-notice strong {
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 2px;
  transition: text-decoration-color 0.12s ease;
}

.row-notice:hover {
  background: var(--nav-hover-bg);
}

.row-notice:hover strong {
  text-decoration-color: currentColor;
}

.row-notice:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: -2px;
}

.row-notice:active {
  background: var(--surface-soft);
}

/*
 * One line of it, however long it was — the rest is behind the click.
 *
 * The width has to be bounded for that to be safe. A single unwrapped line
 * makes the cell ask for the width of the whole notice, and the columns beside
 * it — the dates, the buttons — are squeezed into stacks to pay for it. Bounded,
 * the cell asks for no more than this and clips the rest.
 */
.row-notice {
  max-width: 46ch;
}

.row-notice .muted {
  font-size: 0.8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.notice-body {
  margin: 0.25rem 0 1rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.55;
}

/* The heading is centred, and the line under it follows — a flush-left
   sentence beneath a centred title reads as a caption that came adrift. */
.auth-gate-card .muted,
.auth-gate-card .empty-row {
  padding: 0;
  text-align: center;
}

/* Inputs sit on the card's grey, so they need to come forward, not recede. */
.auth-gate-card input {
  background: var(--surface);
}

.auth-gate-actions {
  display: grid;
  gap: 0.75rem;
  margin-top: 1rem;
}

.auth-gate-actions .search-input,
.auth-gate-actions button {
  width: 100%;
}

/* The whole row is the label, so the words are as clickable as the box. */
.auth-gate-remember {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: -0.15rem 0 0.15rem;
  color: var(--muted);
  font-size: 0.88rem;
  cursor: pointer;
  user-select: none;
}

.auth-gate-remember input {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  min-height: 0;
  margin: 0;
  accent-color: var(--primary-ink);
  cursor: pointer;
}

.auth-gate-remember:hover {
  color: var(--ink);
}

.auth-gate-remember:focus-within {
  color: var(--ink);
}

.auth-gate-remember input:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

.auth-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.65rem;
  min-height: 42px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.65rem 0.75rem;
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 0.9rem;
  font-weight: 700;
}

body.auth-required,
body.access-required {
  min-height: 100vh;
}

body.auth-required .app-shell,
body.access-required .app-shell {
  display: block;
  min-height: 100vh;
}

body.auth-required .sidebar,
body.access-required .sidebar,
body.auth-required .topbar,
body.access-required .topbar {
  display: none;
}

body.auth-required .workspace,
body.access-required .workspace {
  display: grid;
  margin-left: 0;
  min-height: 100vh;
  place-items: center;
  padding: 1.25rem;
}

body.auth-required .tab-panel,
body.access-required .tab-panel {
  width: min(100%, 560px);
}

body.auth-required .auth-gate-panel.active,
body.access-required .auth-gate-panel.active {
  min-height: 0;
  align-content: stretch;
}

.hidden {
  display: none !important;
}

/* The stylesheet gives several images an explicit display, which outranks the
   browser's own rule for the hidden attribute — so an image hidden that way
   would stay on screen, drawing a broken-image glyph. Say it once, here. */
[hidden] {
  display: none !important;
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: var(--modal-backdrop);
}

.modal-card {
  width: min(100%, 680px);
  max-height: calc(100vh - 2.5rem);
  overflow: auto;
}

.attachment-lightbox {
  z-index: 35;
}

.attachment-lightbox-card {
  width: min(100%, 980px);
  max-height: calc(100vh - 2.5rem);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.attachment-lightbox-header {
  flex-shrink: 0;
  align-items: flex-start;
}

.attachment-lightbox-meta {
  margin-top: 0.3rem;
  color: var(--muted);
  font-size: 0.84rem;
  font-weight: 700;
}

.attachment-lightbox-body {
  display: grid;
  place-items: center;
  min-height: 320px;
  max-height: calc(100vh - 220px);
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface-soft);
}

.attachment-lightbox-body img,
.attachment-lightbox-body iframe {
  display: block;
  width: 100%;
  border: 0;
}

.attachment-lightbox-body img {
  height: auto;
  max-height: calc(100vh - 240px);
  object-fit: contain;
}

.attachment-lightbox-body iframe {
  height: min(72vh, 760px);
}

.attachment-lightbox-empty {
  padding: 2rem;
  color: var(--muted);
  text-align: center;
}

.attachment-lightbox-empty .loading-spinner {
  display: inline-block;
  vertical-align: -0.15rem;
  margin-right: 0.4rem;
}

/* ------------------------------------------------------ office previews
   A Word file is laid out as pages by docx-preview and keeps its own
   colours — a document is read as printed, in both themes, so the pages
   stay white on the lightbox's grey. A workbook is one plain table per
   sheet, in the app's own type. */
.attachment-lightbox-body .office-preview {
  width: 100%;
  height: min(72vh, 760px);
  overflow: auto;
  align-self: stretch;
}

.attachment-lightbox-body .office-preview.is-loading {
  display: grid;
  place-items: center;
}

.office-preview .docx-wrapper {
  background: transparent;
  padding: 1rem 0.5rem;
}

.office-preview .docx-wrapper > section.docx {
  color: #10151b;
  background: #ffffff;
  box-shadow: 0 1px 4px rgba(16, 21, 27, 0.18);
  margin-bottom: 1rem;
  max-width: 100%;
  overflow-x: auto;
}

.sheet-tabs {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0.5rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.sheet-tab {
  min-height: 32px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  padding: 0.3rem 0.8rem;
  background: var(--surface);
  color: var(--ink);
  font-size: 0.8rem;
  font-weight: 600;
}

.sheet-tab:hover {
  background: var(--surface-soft);
}

.sheet-tab.active {
  border-color: var(--primary-ink);
  background: var(--primary-soft);
  color: var(--primary-ink);
}

.sheet-preview {
  max-height: none;
  background: var(--surface);
}

.sheet-preview table {
  width: auto;
  min-width: 100%;
  border-collapse: collapse;
  font-size: 0.82rem;
}

.sheet-preview td,
.sheet-preview th {
  border: 1px solid var(--line);
  padding: 0.3rem 0.55rem;
  white-space: nowrap;
  vertical-align: top;
}

.attachment-lightbox-actions {
  flex-shrink: 0;
  justify-content: flex-end;
}

/* The stat strip. The reference runs its figures in a single open row divided
   by hairlines, with a circular icon badge leading each one — no cards, no
   fills, no box per number. */
.metric-grid {
  display: grid;
  /* Explicit counts, not auto-fit. The dashboard shows six figures and the
     compact strips show four, so the column count is held to a divisor of
     those — six or three, four or two — and a row is never left holding a
     single lonely figure with four empty columns beside it.

     The count is a single property so there is one place that decides it. The
     rules further down set it from the width of the window, which is the best
     a stylesheet can do; the script sets it from the width of the strip
     itself, which is the figure that actually matters, and being an inline
     property it wins. Either way this declaration is the only one that draws
     the columns. */
  --metric-columns: 6;
  grid-template-columns: repeat(var(--metric-columns), minmax(0, 1fr));
  gap: 0;
  border: 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  /* Nothing paints the grid itself. Filling it and letting a 1px gap show
     through looked identical while every row was full, but a wrapped row
     leaves empty columns, and those painted as one large grey block. */
  background: transparent;
  overflow: hidden;
}

.metric-grid.small {
  --metric-columns: 4;
}

/* Badge on the left, three stacked lines on the right: label, figure, note.
   The reference sets its note beside the figure, which works for "+8 tasks"
   but not for "All recorded payments" — six of those across a row clip every
   one of them. Stacking keeps the badge signature and loses no words. */
.metric-card {
  position: relative;
  display: grid;
  grid-template-columns: var(--badge) minmax(0, 1fr);
  align-items: center;
  align-content: center;
  column-gap: 0.8rem;
  row-gap: 0.15rem;
  min-height: 104px;
  border: 0;
  border-radius: 0;
  padding: 1.15rem 1.35rem;
  background: transparent;
  /* Each figure casts its own divider one pixel to its left. The card that
     starts a row sits at x=0, so its line falls outside the grid and is
     clipped by the overflow above — which is what makes this work for a
     wrapped row, and for any number of figures, without counting them. */
  box-shadow: -1px 0 0 var(--line);
}

/* The circular badge. It spans both text rows in the left column and takes its
   colour from the card's tone, tinted rather than saturated so a row of six
   does not turn into bunting. */
.metric-card::after {
  content: attr(data-icon);
  display: grid;
  place-items: center;
  grid-column: 1;
  grid-row: 1 / span 3;
  width: var(--badge);
  height: var(--badge);
  border-radius: 50%;
  background: var(--surface-soft);
  color: var(--muted);
  font-family: "boxicons";
  font-size: 1.3rem;
  line-height: 1;
}

/* A badge with its own artwork: the circle stays on ::after (empty, since the
   font glyph is blank) and the SVG is masked onto ::before above it — a mask
   clips everything its element paints, so it cannot share the circle. */
.metric-card[data-icon-svg]::before {
  content: "";
  z-index: 1;
  grid-column: 1;
  grid-row: 1 / span 3;
  justify-self: center;
  width: 1.5rem;
  height: 1.5rem;
  background: var(--muted);
  -webkit-mask: center / contain no-repeat;
  mask: center / contain no-repeat;
}

.metric-card[data-icon-svg="income"]::before {
  -webkit-mask-image: url("assets/ui-kit/icons/income.svg");
  mask-image: url("assets/ui-kit/icons/income.svg");
}

.metric-card span:first-of-type {
  order: -1;
  grid-column: 2;
  grid-row: 1;
  color: var(--muted);
  font-size: 0.88rem;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: 0;
  text-transform: none;
}

.metric-card strong {
  grid-column: 2;
  grid-row: 2;
  font-size: 1.55rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.028em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.metric-card span:last-of-type {
  grid-column: 2;
  grid-row: 3;
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.35;
}

.metric-card span:first-of-type:last-of-type {
  grid-column: 2;
  grid-row: 1;
}

/* Tone tints the badge instead of adding a dot before the label. */
.metric-card[data-tone] span:first-of-type::before {
  content: none;
}

.metric-card[data-tone="success"]::after { background: color-mix(in srgb, var(--success) 17%, var(--surface)); }
.metric-card[data-tone="warning"]::after { background: color-mix(in srgb, var(--warning) 20%, var(--surface)); }
.metric-card[data-tone="danger"]::after  { background: color-mix(in srgb, var(--danger) 16%, var(--surface)); }
.metric-card[data-tone="teal"]::after    { background: color-mix(in srgb, var(--teal) 16%, var(--surface)); }
.metric-card[data-tone="primary"]::after { background: var(--primary-soft); }

.metric-card[data-tone="success"]::after { color: var(--success-ink); }
.metric-card[data-tone="warning"]::after { color: var(--warning-ink); }
.metric-card[data-tone="danger"]::after  { color: var(--danger-ink); }
.metric-card[data-tone="teal"]::after    { color: var(--teal-ink); }
.metric-card[data-tone="primary"]::after { color: var(--primary-ink); }

.metric-card[data-tone="success"] span:last-of-type { color: var(--success-ink); }
.metric-card[data-tone="warning"] span:last-of-type { color: var(--warning-ink); }
.metric-card[data-tone="danger"] span:last-of-type  { color: var(--danger-ink); }

.content-grid {
  display: grid;
  gap: 1rem;
}

.two-column {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

.form-and-list {
  grid-template-columns: minmax(320px, 0.45fr) minmax(0, 1fr);
}

/* Stacked, full width, with the same rule between panels the page draws
   between its top-level blocks — so a wide table gets the whole width and the
   eye still finds where one ends. */
.content-grid.one-column {
  grid-template-columns: minmax(0, 1fr);
}

.content-grid.one-column > :not(.hidden):has(~ :not(.hidden)) {
  border-bottom: 1px solid var(--line);
  padding-bottom: 1.2rem;
}

.panel {
  min-width: 0;
  padding: 0;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.2rem;
}

/* A header carrying only its controls: the panel's name is the page's name,
   so the row holds the action and nothing else, kept at the right where the
   eye already looks for it. */
.panel-header-actions-only {
  justify-content: flex-end;
  margin-bottom: 0.9rem;
}

/* Section headings are large and dark, like "Performance" and "Current Tasks".
   The eyebrow above them shrinks to a quiet label. */
.panel h3 {
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -0.024em;
}

/* The surviving eyebrows are the ones with no heading beneath them — the
   member profile's section labels — so they carry the heading's weight. */
.panel > .eyebrow,
.member-profile-section > .eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  color: var(--muted);
}

/* Panels need air between them now that nothing draws their edges. */
.content-grid {
  gap: 2.4rem;
}

/*
 * Dividers between blocks.
 *
 * Panels carry no border of their own, so on a long page one section ran into
 * the next with nothing but white space to say where the first ended — worst
 * where a short table left a tall empty gap and the eye read it as the end of
 * the page rather than the end of a column.
 *
 * Every rule here sits in the MIDDLE of the space that was already there:
 * the container's gap is halved and the same amount is given back as padding
 * above the line. Nothing moves; a line simply appears where the gap was.
 */
.tab-panel.active {
  display: grid;
  gap: 1.2rem;
}

/*
 * The rule goes UNDER a block, and only when another visible block follows it.
 * Stated that way round it needs no notion of "the first block", which is the
 * part that cannot be written: whole panels are hidden for the scope the
 * viewer is in, so the first block in the DOM is frequently invisible and the
 * first one on screen is some later sibling. Keying off :first-child drew a
 * rule across the top of Dues and Expenses with nothing above it.
 *
 * :has(~ …) is what makes this work — the general sibling combinator skips the
 * hidden panels and the hidden modal backdrops sitting between the real ones,
 * where + would stop at the first of them.
 */
.tab-panel.active
  > :is(.panel, .content-grid):not(.hidden):has(~ :is(.panel, .content-grid):not(.hidden)) {
  padding-bottom: 1.2rem;
}

/* The rule itself, everywhere except immediately above the stat strip — the
   strip brings its own top border, and two of them 1.2rem apart read as a
   mistake. Written as one selector rather than an override because the
   override would have had to out-specify the :has() above it. */
.tab-panel.active
  > :is(.panel, .content-grid):not(.hidden):not(:has(+ .metric-grid)):has(~ :is(.panel, .content-grid):not(.hidden)) {
  border-bottom: 1px solid var(--line);
}

/* The stat strip draws a rule of its own and must not be padded from the
   inside — that would open a band of empty grey above its own border. It gives
   the block beneath it the other half of the gap instead. */
.tab-panel.active > .metric-grid + * {
  margin-top: 1.2rem;
}

/*
 * Two panels sharing one column of a two-column row. The column is a block in
 * its own right — it takes the vertical divider beside it like any other — and
 * inside it the panels are separated the same way everything else is, by a
 * rule sitting in the middle of the gap.
 */
.panel-stack {
  display: grid;
  align-content: start;
  gap: 1.2rem;
  min-width: 0;
}

.panel-stack > .panel + .panel {
  border-top: 1px solid var(--line);
  padding-top: 1.2rem;
}

/* Side by side, the divider is vertical and runs the full height of the row —
   past the end of a short table, which is exactly where the eye needs telling
   that the column has ended rather than the page. Grid items stretch by
   default, so the border does this without anyone declaring a height. */
@media (min-width: 1181px) {
  .content-grid.two-column {
    column-gap: 1.5rem;
  }

  .content-grid.two-column > :not(.hidden):has(~ :not(.hidden)) {
    border-right: 1px solid var(--line);
    padding-right: 1.5rem;
  }
}

/*
 * A row whose right column is a fixed-size object rather than a share of the
 * width. The membership card is 480px and stays 480px, so a half-and-half
 * split left a band of empty page beside it; giving the card column its own
 * width and the rest to the left one closes that up.
 */
@media (min-width: 1181px) {
  .content-grid.two-column.card-aside {
    grid-template-columns: minmax(0, 1fr) auto;
  }
}

/* Below the breakpoint the columns stack and the same relationship is a
   horizontal rule. */
@media (max-width: 1180px) {
  .content-grid.two-column {
    row-gap: 1.2rem;
  }

  .content-grid.two-column > :not(.hidden):has(~ :not(.hidden)) {
    border-bottom: 1px solid var(--line);
    padding-bottom: 1.2rem;
  }
}

/* Modal cards are the exception: floating over the sheet, they must read as
   objects, so they keep their fill, radius and lift. */
.modal-card {
  border: 0;
  border-radius: var(--radius-xl);
  padding: 1.9rem 2rem 2rem;
  background: var(--surface);
  box-shadow: var(--shadow-raised);
}

.stacked-form {
  display: grid;
  gap: 0.85rem;
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem;
}

/* A field with a note under it makes its row taller, and without this the
   field beside it stretched to match — a 90px tall Method dropdown next to a
   normal Amount box. Each label sits at the top of its row instead. */
.field-grid > label {
  align-content: start;
}

.mode-toggle {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.45rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.35rem;
  background: var(--surface-soft);
}

.mode-toggle label {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 34px;
  border-radius: 7px;
  padding: 0.45rem 0.6rem;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  text-align: center;
}

.mode-toggle label:has(input:checked) {
  background: var(--surface-raised);
  box-shadow: var(--mode-active-shadow);
  color: var(--mode-active-ink);
}

.mode-toggle input {
  width: auto;
  margin: 0;
  accent-color: var(--primary);
}

.form-section-title {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--primary-ink);
  font-size: 0.78rem;
  font-weight: 900;
  text-transform: uppercase;
}

.form-section-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

label {
  display: grid;
  gap: 0.4rem;
  color: var(--ink);
  font-size: 0.85rem;
  font-weight: 600;
}

.inline-check {
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

.inline-check input {
  width: auto;
  margin: 0;
}

.field-help {
  color: var(--muted);
  font-size: 0.76rem;
  font-weight: 600;
}

/* The way out of a read-only field, set in the help line under it so it reads
   as a remark about the field rather than a second control competing with it. */
.field-unlock {
  display: inline;
  margin-left: 0.35rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--primary-ink);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.field-unlock:hover {
  color: var(--ink);
}

.field-unlock:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
  border-radius: 3px;
}

/*
 * A field the form is filling in for you.
 *
 * Every input here already sits on a soft grey fill, so making a read-only one
 * grey said nothing at all. It loses the fill instead and keeps a dashed edge:
 * a figure stated on the sheet rather than a box waiting to be typed in. The
 * weight is the figure's, not a form field's, for the same reason.
 */
input[readonly] {
  border-color: var(--line);
  border-style: dashed;
  background: transparent;
  color: var(--ink);
  font-weight: 700;
  cursor: default;
}

input[readonly]:hover,
input[readonly]:focus {
  border-color: var(--line);
  box-shadow: none;
}

input,
select,
textarea {
  width: 100%;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  padding: 0.72rem 0.95rem;
  background: var(--surface-soft);
  color: var(--ink);
  min-width: 0;
  transition:
    background-color 160ms ease,
    border-color 160ms ease;
}

input:hover,
select:hover,
textarea:hover {
  border-color: var(--line-strong);
}

textarea {
  border-radius: var(--radius-control);
}

input::placeholder,
textarea::placeholder {
  color: var(--placeholder);
}

textarea {
  resize: vertical;
}

.search-input {
  width: min(260px, 100%);
  border-radius: var(--radius-control);
}

/* Filter and range dropdowns are outlined pills, matching the "01-07 May" and
   "Week" controls in the reference. Form fields inside a <label> keep the
   softer filled rectangle so a form still reads as a form. */
.panel-header select,
.toolbar select,
.topbar select,
.member-toolbar select,
.table-pagination select {
  border-radius: var(--radius-control);
  border-color: var(--line-strong);
  background: var(--surface);
  padding-inline: 1.05rem;
}

.table-wrap {
  width: 100%;
  overflow-x: auto;
  border: 0;
  /* Square, deliberately. The radius was left over from when a table sat in a
     card with its own edges; with nothing drawn around it there is no corner
     to soften, and the overflow that scrolls the table also clips to that
     radius — which bit the corners off the first and last header cells, whose
     text is flush with the table edge so it lines up with the column beneath. */
  border-radius: 0;
  background: transparent;
}

.table-wrap.compact {
  max-height: 380px;
}

/* A table tall enough to scroll keeps its header in view. The border is drawn
   as an inset shadow rather than a border: with border-collapse the bottom
   edge belongs to the row below as much as to the header, and it is left
   behind the moment the header starts moving. */
.table-wrap.compact thead th,
.member-profile-card .member-profile-section thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  border-bottom-color: transparent;
  box-shadow: inset 0 -1px 0 var(--line);
}

table {
  width: 100%;
  border-spacing: 0;
  /* Squeeze to the container when it is narrower than the natural width,
     rather than forcing a scrollbar that hides the last column. Wide tables
     still scroll because .table-wrap keeps overflow-x. */
  min-width: min(720px, 100%);
  border-collapse: collapse;
  background: var(--surface);
}

/* Rows are one line each now, so a cell needs only enough air to keep the
   lines apart — the old 0.95rem above and below turned ten rows into a page. */
th,
td {
  padding: 0.45rem 0.8rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: middle;
}

td {
  font-size: 0.95rem;
}

/* The first and last cells used to be flush with the table's edge so their
   text lined up with the panel heading above. Once the header row gained a
   filled band, flush meant the words touched the end of it — so both ends get
   the same air as every other cell. */
th:first-child,
td:first-child {
  padding-left: 0.9rem;
}

th:last-child,
td:last-child {
  padding-right: 0.9rem;
}

/* Headers sit on a grey band: still small, tracked and muted, so the eye goes
   to the rows, but the band gives every table a definite top edge — and is
   what makes a header stuck over a scrolling body opaque rather than letting
   the rows read straight through it. */
th {
  background: var(--table-head-background);
  border-bottom-color: var(--line);
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  white-space: nowrap;
}

tbody tr:last-child td {
  border-bottom: 0;
}

tbody tr:hover td {
  background: var(--row-hover);
}

/* A row for something already past — an expired notice, a meeting held last
   month — is kept and dimmed rather than hidden, so the record stays whole
   while the eye goes to what is still live. It lifts back to full strength on
   hover, because a dim row still has to be readable when you reach for it. */
tbody tr.row-muted td {
  opacity: 0.62;
}

tbody tr.row-muted:hover td,
tbody tr.row-muted:focus-within td {
  opacity: 1;
}

/* Names lead their row at full ink weight; everything else is supporting. */
tbody td:first-child strong,
tbody td:first-child .member-name-button,
tbody td:first-child .lodge-name-button {
  font-weight: 600;
}

/* The identity column carries a name plus a secondary line, so it needs room
   before the browser starts breaking names across lines. */
tbody td:first-child {
  min-width: 168px;
}

/* …unless the first column is a date, which needs exactly a date's width. */
table:has(> thead th.date-column:first-child) > tbody > tr > td:first-child {
  min-width: 0;
}

/* The member directory's lodge column holds "Name No. NN", which breaks onto
   three lines in a narrow column. */
#members tbody td:nth-child(3) {
  min-width: 150px;
}

td.numeric,
th.numeric {
  font-variant-numeric: tabular-nums;
}

.table-sort {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  width: auto;
  min-height: 0;
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-transform: inherit;
  cursor: pointer;
}

.table-sort.numeric {
  justify-content: flex-end;
  margin-left: auto;
}

.table-sort.active {
  color: var(--primary-ink);
}

.sort-indicator {
  min-width: 2.3rem;
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 900;
  text-align: left;
  text-transform: none;
}

tr:last-child td {
  border-bottom: 0;
}

.numeric {
  text-align: right;
}

/* Seven bordered icon buttons per row read as a control panel and wrapped
   onto three lines, tripling row height. Borderless and muted by default,
   they sit quietly until the row is hovered — the row stays one line and the
   member's data leads. */
/* A button inside a row is a row-scale control: full-size pills in every
   Actions cell turn a five-row table into a wall of buttons. */
td button {
  min-height: 32px;
  padding: 0.35rem 0.9rem;
  font-size: 0.85rem;
  font-weight: 600;
}

.row-actions {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.15rem;
}

.row-actions button {
  min-height: 28px;
  padding: 0.3rem 0.5rem;
  font-size: 0.78rem;
}

.row-actions .table-icon-button {
  width: 30px;
  min-width: 30px;
  min-height: 30px;
  padding: 0;
  border-radius: 50%;
  border-color: transparent;
  background: transparent;
  color: var(--muted);
  opacity: 0.75;
}

tr:hover .row-actions .table-icon-button {
  opacity: 1;
}

.row-actions .table-icon-button:hover {
  border-color: var(--line-strong);
  background: var(--surface-soft);
  color: var(--ink);
  opacity: 1;
}

.row-actions .table-icon-button.danger:hover {
  border-color: color-mix(in srgb, var(--danger) 40%, var(--line));
  color: var(--danger-ink);
}

.table-icon-button {
  display: inline-grid;
  place-items: center;
  text-decoration: none;
}

.row-actions .teleport-action-button {
  width: 28px;
  min-width: 28px;
  padding: 0;
  border-color: transparent;
  background: transparent;
  color: var(--primary-ink);
}

/*
 * The figure in a suit — signing in AS someone, rather than the person-badge
 * that means administrator. It used to borrow the Administrators icon, which
 * made "manage administrators" and "become this member" look like the same
 * action. Masked, so it takes currentColor like every other row icon.
 *
 * The artwork is drawn in strokes rather than filled shapes, and a mask keeps
 * only what is painted — so it needs a little more room than a solid glyph to
 * read at this size.
 */
.teleport-svg-icon {
  width: 1.3rem;
  height: 1.3rem;
  background: currentColor;
  -webkit-mask: url("assets/ui-kit/icons/impersonate.svg") center / contain no-repeat;
  mask: url("assets/ui-kit/icons/impersonate.svg") center / contain no-repeat;
}

/* Up to ten controls sit on a member row. In one line they ran past the
   panel's edge, and the table's horizontal scroll is invisible on a Mac until
   it moves — so the lodge admin's "Login as member" (sixth in line) looked
   as if it did not exist. Five to a row, then a second row. */
.member-row-actions {
  flex-wrap: wrap;
  justify-content: flex-end;
  /* Four 30px circles and their gaps; fixed, or the table gives the column
     only as much as three of them and wraps the rest into a third row. Four
     rather than five since View and the office rosette left the row: seven
     controls sit as four and three instead of five and a straggler. */
  width: 9.05rem;
  min-width: 9.05rem;
  margin-left: auto;
}

/* The widened control block would push the table 6px past its panel at a
   common laptop width; the cell's own left padding is where the room comes
   from — the figures are right-aligned, so nothing is crowded. */
td:has(> .member-row-actions) {
  padding-left: 0.3rem;
}

.attachment-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 0.65rem 0.75rem;
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 0.84rem;
}

.attachment-summary strong {
  display: block;
  color: var(--ink);
  font-size: 0.9rem;
}

.attachment-actions {
  display: flex;
  align-items: center;
  gap: 0.45rem;
}

.table-action-icon {
  display: block;
  font-family: "boxicons";
  font-size: 1.15rem;
  line-height: 1;
}

.icon-open::before {
  content: "\eae1";
}

.icon-admin::before {
  content: "\ec68";
}

.icon-add-member::before {
  content: "\ec68";
}

.icon-assign-lodge::before {
  content: "\ec4e";
}

/* The two figures that open the brother's wallet: what he holds and what he
   has asked for. Side by side, because the second explains the first when it
   is lower than he expected. */
.wallet-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: 0.75rem;
  margin-bottom: 1rem;
}

/* A credit reads as a credit at a glance; a debit is left in the ordinary ink
   rather than shouted in red, because spending your own money is not an
   error. */
.wallet-positive {
  color: var(--success-ink, var(--ink));
  font-weight: 700;
}

/* Which office the person signed in holds, beside his name. Quiet: it is a
   fact about the session, not a control. */
.office-badge {
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.icon-edit::before {
  content: "\eabf";
}

.icon-payment::before {
  content: "\ea87";
}

/* "Record Payment" on the dues tables: the receipt the user supplied
   (assets/ui-kit/icons/receipt.svg, copied from assets/receipt.svg), masked
   so it takes currentColor. The control is the picture and nothing else — no plate, no
   border, no padding — because a boxed button was taking half the column. */
.icon-money {
  width: 1.45rem;
  height: 1.45rem;
  background: currentColor;
  -webkit-mask: url("assets/ui-kit/icons/receipt.svg") center / contain no-repeat;
  mask: url("assets/ui-kit/icons/receipt.svg") center / contain no-repeat;
}

.icon-money::before {
  content: none;
}

.money-button,
.money-button:hover,
.money-button:active {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  min-width: 0;
  height: 2rem;
  min-height: 0;
  margin: -0.25rem 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  box-shadow: none;
  color: var(--ink);
  transform: none;
}

.money-button:hover {
  color: var(--primary-ink);
}

.money-button:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--primary-ink) 34%, transparent);
  outline-offset: 2px;
}

/* A blocked circle for suspend, a restoring arrow for reinstate — the pair
   reads as one reversible action rather than two unrelated controls. */
.icon-suspend::before {
  content: "\ec87";
}

.icon-reinstate::before {
  content: "\eb2c";
}

.icon-reset-password::before {
  content: "\eb28";
}

.icon-login-disable::before {
  content: "\eb49";
}

.icon-login-enable::before {
  content: "\eb4b";
}

.icon-receipt::before {
  content: "\ebd5";
}

.icon-delete::before {
  content: "\ec50";
}

/* Minutes, Download and the lock on a paid-against due period are drawn
   rather than taken from the font, whose glyph names are opaque. Masked, so
   they take currentColor like the glyphs beside them. */
.icon-minutes,
.icon-download,
.icon-lock {
  width: 1.15rem;
  height: 1.15rem;
  background: currentColor;
}

.icon-minutes::before,
.icon-download::before,
.icon-lock::before {
  content: none;
}

.icon-minutes {
  -webkit-mask: url("assets/ui-kit/icons/minutes.svg") center / contain no-repeat;
  mask: url("assets/ui-kit/icons/minutes.svg") center / contain no-repeat;
}

.icon-download {
  -webkit-mask: url("assets/ui-kit/icons/download.svg") center / contain no-repeat;
  mask: url("assets/ui-kit/icons/download.svg") center / contain no-repeat;
}

.icon-lock {
  -webkit-mask: url("assets/ui-kit/icons/lock.svg") center / contain no-repeat;
  mask: url("assets/ui-kit/icons/lock.svg") center / contain no-repeat;
}

/* The lock takes Delete's place on a due period something has been paid
   against: a mark, not a control, so it never lights up under the pointer. */
.row-actions .locked-mark {
  width: 30px;
  min-width: 30px;
  min-height: 30px;
  color: var(--muted);
  opacity: 0.75;
  cursor: help;
}

/*
 * A date column is as wide as a date and no wider. The header carries the
 * class; the cells beneath it are found by position, so the renderers need
 * not know. Dates never wrap — a slash is a place the browser would break.
 */
th.date-column {
  width: 1%;
  white-space: nowrap;
}

table:has(> thead th.date-column:nth-child(1)) > tbody > tr > td:nth-child(1),
table:has(> thead th.date-column:nth-child(2)) > tbody > tr > td:nth-child(2),
table:has(> thead th.date-column:nth-child(3)) > tbody > tr > td:nth-child(3),
table:has(> thead th.date-column:nth-child(4)) > tbody > tr > td:nth-child(4),
table:has(> thead th.date-column:nth-child(5)) > tbody > tr > td:nth-child(5),
table:has(> thead th.date-column:nth-child(6)) > tbody > tr > td:nth-child(6) {
  white-space: nowrap;
}

/*
 * A listing row that opens its record. The pointer says so; the outline says
 * so to a keyboard, which reaches the row by Tab and opens it with Enter.
 */
tr.record-row {
  cursor: pointer;
}

tr.record-row:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--primary-ink) 34%, transparent);
  outline-offset: -3px;
}

.record-card {
  width: min(100%, 640px);
}

.record-details-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
  gap: 0.75rem 1.5rem;
  margin-bottom: 1rem;
}

.record-details-grid .record-field {
  padding-bottom: 0.5rem;
}

.record-details-grid .record-field strong {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.record-lines {
  display: grid;
  gap: 0.4rem;
  margin-bottom: 1rem;
}

.record-lines td,
.record-lines th {
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}

/* The bundled boxicons build has no reliable "eye" codepoint, so the view
   action masks a dedicated eye.svg. (show.svg, despite the name, is the
   sidebar expand icon — a panel, not an eye.) Masked so it inherits
   currentColor like the glyphs around it. */
.icon-view {
  width: 1.15rem;
  height: 1.15rem;
  background: currentColor;
  -webkit-mask: url("assets/ui-kit/icons/eye.svg") center / contain no-repeat;
  mask: url("assets/ui-kit/icons/eye.svg") center / contain no-repeat;
}

/* The member name is the obvious thing to click, so it opens the profile too.
   A button rather than a link: it triggers an action, not navigation. */
/* A name in the first column doubles as the way into the record it names —
   the row's view button is still there, but the name is what people aim at. */
.member-name-button,
.lodge-name-button {
  display: block;
  width: auto;
  min-height: 0;
  border: 0;
  border-radius: 3px;
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}

.member-name-button:hover,
.lodge-name-button:hover {
  color: var(--primary-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.member-name-button:focus-visible,
.lodge-name-button:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

/* -------------------------------- chat ------------------------------- */

/* The count rides on the nav item, so an unread message is visible from any
   section without opening anything. */
.nav-badge {
  margin-left: auto;
  min-width: 20px;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--danger);
  color: #ffffff;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}

/*
 * Messages is a dock, not a page.
 *
 * A brother answering the Secretary is nearly always in the middle of
 * something else — reading an invoice, checking a date — and a full section
 * made him leave it and find his way back. The dock slides in over the right
 * edge, keeps the page underneath exactly where it was, and closes to nothing
 * but a launcher in the corner.
 *
 * It is one column wide, so the list and the thread take turns in it rather
 * than sitting side by side; `data-view` on the dock says which is showing.
 */
.chat-launcher {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 14;
  display: grid;
  place-items: center;
  width: 54px;
  height: 54px;
  min-height: 0;
  padding: 0;
  border: 1px solid var(--primary);
  border-radius: 50%;
  background: var(--primary);
  color: var(--primary-on);
  box-shadow: 0 10px 26px rgba(15, 23, 42, 0.28);
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, opacity 0.15s ease;
}

.chat-launcher:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 32px rgba(15, 23, 42, 0.34);
  background: var(--primary-dark);
  border-color: var(--primary-dark);
}

.chat-launcher:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 3px;
}

.chat-launcher:active {
  transform: translateY(0);
  background: var(--primary-dark);
}

/* While the panel is open the launcher is behind it. It fades out rather than
   disappearing, so the corner does not blink on every open and close. */
body.chat-dock-open .chat-launcher {
  opacity: 0;
  transform: scale(0.85);
  pointer-events: none;
}

.chat-launcher-icon {
  width: 1.5rem;
  height: 1.5rem;
  background: currentColor;
  -webkit-mask: var(--icon-chat) center / contain no-repeat;
  mask: var(--icon-chat) center / contain no-repeat;
}

/* The count sits on the rim of the button rather than inside it, so it is
   legible without shrinking the icon it belongs to. */
.chat-launcher-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 20px;
  padding: 0 0.3rem;
  border: 2px solid var(--surface);
  border-radius: 999px;
  background: var(--danger);
  color: #ffffff;
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

.chat-dock {
  position: fixed;
  /* Zero, except while an impersonation banner is on screen — see
     syncChatDockOffset(), which keeps the way out of the account uncovered. */
  top: var(--chat-dock-top, 0px);
  right: 0;
  z-index: 15;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  width: min(390px, 100vw);
  height: calc(100vh - var(--chat-dock-top, 0px));
  height: calc(100dvh - var(--chat-dock-top, 0px));
  padding: 0;
  border-left: 1px solid var(--line);
  background: var(--surface);
  box-shadow: -18px 0 44px rgba(15, 23, 42, 0.18);
  /* Off the edge rather than out of the document: it stays laid out, so the
     slide has somewhere to come from and somewhere to go back to. */
  transform: translateX(102%);
  visibility: hidden;
  transition: transform 0.22s ease, visibility 0.22s;
}

.chat-dock.is-open {
  transform: translateX(0);
  visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
  .chat-dock {
    transition: none;
  }

  .chat-launcher {
    transition: none;
  }
}

.chat-dock-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.85rem 0.9rem;
  border-bottom: 1px solid var(--line);
}

.chat-dock-title {
  flex: 1 1 auto;
  min-width: 0;
}

.chat-dock-title h3 {
  margin: 0;
  font-size: 1rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-dock-title p {
  margin: 0;
  font-size: 0.78rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-dock-title p:empty {
  display: none;
}

.chat-dock-head button {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 32px;
  min-width: 32px;
  height: 32px;
  min-height: 32px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--muted);
  box-shadow: none;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.chat-dock-head button:hover,
.chat-dock-head button:focus-visible {
  background: var(--surface-soft);
  border-color: var(--line-strong);
  color: var(--ink);
}

.chat-dock-head button:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

.chat-dock-head button:active {
  background: var(--line-strong);
}

/* Drawn rather than lettered, so the three controls in this header carry the
   same optical weight as each other and as the dialog close beside them. */
.chat-dock-back::before {
  content: "";
  width: 9px;
  height: 9px;
  border-left: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateX(2px) rotate(45deg);
}

.chat-dock-action::before,
.chat-dock-action::after {
  content: "";
  position: absolute;
  width: 13px;
  height: 1.5px;
  border-radius: 2px;
  background: currentColor;
}

.chat-dock-action {
  position: relative;
}

/* The "new message" control is a plus; the close beside it is the same two
   bars turned 45 degrees. */
.chat-dock-action:not(.chat-dock-close)::after {
  transform: rotate(90deg);
}

.chat-dock-close::before {
  transform: rotate(45deg);
}

.chat-dock-close::after {
  transform: rotate(-45deg);
}

.chat-dock .chat-list,
.chat-dock .chat-thread {
  padding: 0.75rem 0.8rem;
}

/* One column, two views. */
.chat-dock[data-view="list"] .chat-thread,
.chat-dock[data-view="list"] .chat-composer,
.chat-dock[data-view="list"] .chat-read-only,
.chat-dock[data-view="list"] .chat-dock-back,
.chat-dock[data-view="thread"] .chat-list {
  display: none;
}

.chat-dock .chat-composer {
  padding: 0.7rem 0.8rem;
  border-top: 1px solid var(--line);
}

.chat-dock .chat-composer textarea {
  min-height: 40px;
  max-height: 8rem;
}

/* What stands where the composer would be when an administrator is reading a
   brother's inbox. It occupies the same row, so the thread above it does not
   change height when the mode does. */
.chat-read-only {
  margin: 0;
  padding: 0.7rem 0.8rem;
  border-top: 1px solid var(--line);
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 0.82rem;
}

/* In that mode the header's plus would offer something the panel cannot do. */
.chat-dock.is-read-only .chat-dock-action:not(.chat-dock-close) {
  display: none;
}

.chat-list {
  overflow-y: auto;
  display: grid;
  align-content: start;
  gap: 0.35rem;
}

.chat-item {
  position: relative;
  display: grid;
  gap: 0.2rem;
  width: 100%;
  min-height: 0;
  padding: 0.7rem 2.2rem 0.7rem 0.75rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

.chat-item:hover {
  background: var(--nav-hover-bg);
}

.chat-item.is-active {
  border-color: var(--line-strong);
  background: var(--surface-soft);
}

/* The selected item is already filled, so hovering it cannot answer with a
   fill — the border is what moves instead. Every item in the list responds to
   the pointer, including the one you are already in. */
.chat-item.is-active:hover {
  border-color: var(--ink);
}

.chat-item:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

.chat-item-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
}

.chat-item-title {
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-item-time {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 0.75rem;
}

/* One line of the last thing said, however long it was. */
.chat-item-preview {
  color: var(--muted);
  font-size: 0.86rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-item-unread {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  min-width: 20px;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: var(--danger);
  color: #ffffff;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 20px;
  text-align: center;
}

.chat-thread {
  overflow-y: auto;
  display: grid;
  align-content: start;
  gap: 0.5rem;
  padding: 0.25rem 0.1rem;
}

.chat-message {
  display: grid;
  gap: 0.15rem;
  justify-self: start;
  max-width: min(90%, 40rem);
  padding: 0.6rem 0.8rem;
  border-radius: var(--radius);
  background: var(--surface-soft);
}

/*
 * Your own words on the other side and in the accent — the oldest convention
 * in messaging, and why nobody has to read a name to know who spoke.
 *
 * The dark theme's --primary-soft is a translucent wash, which is right over a
 * panel and wrong here: a message bubble sits on the panel, so the wash would
 * be composited twice and the ink measured against a colour the eye never
 * sees. Both themes get an opaque plate of their own.
 */
.chat-message.is-mine {
  justify-self: end;
  background: var(--chat-mine-background);
  border: 1px solid var(--line-strong);
}

.chat-message-who {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 700;
}

.chat-message-body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.chat-message-time {
  color: var(--muted);
  font-size: 0.7rem;
  justify-self: end;
}

.chat-composer {
  display: flex;
  align-items: flex-end;
  gap: 0.6rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
}

.chat-composer textarea {
  flex: 1 1 auto;
  min-height: 44px;
  resize: vertical;
}

.chat-people {
  display: grid;
  gap: 0.15rem;
  max-height: 18rem;
  overflow-y: auto;
  padding-right: 0.25rem;
}

.chat-person {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0.5rem;
  border-radius: var(--radius);
  cursor: pointer;
}

.chat-person:hover {
  background: var(--nav-hover-bg);
}

.chat-person input {
  width: 16px;
  height: 16px;
  min-height: 0;
  margin: 0;
  accent-color: var(--primary-ink);
}

.chat-person-name {
  font-weight: 600;
}

.chat-person-lodge {
  margin-left: auto;
  color: var(--muted);
  font-size: 0.8rem;
}

/* On a phone the dock is the screen: a 390px panel beside a 380px page is
   neither one thing nor the other. The launcher steps in from the edge so it
   clears a thumb resting on the corner. */
@media (max-width: 520px) {
  .chat-dock {
    width: 100vw;
    border-left: 0;
  }

  .chat-launcher {
    right: 1rem;
    bottom: 1rem;
  }
}

/* ------------------------------ member profile ----------------------- */

.member-profile-card {
  width: min(100%, 940px);
  display: grid;
  gap: 1.5rem;
}

.member-profile-identity {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  min-width: 0;
}

.member-profile-avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface-soft) center / cover no-repeat;
  color: var(--muted);
  font-size: 0.95rem;
  font-weight: 600;
}

.member-profile-header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.member-profile-identity h3 {
  margin: 0.1rem 0 0.15rem;
  font-size: 1.05rem;
}

.member-profile-ledger {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.member-profile-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
}

.member-profile-section {
  display: grid;
  gap: 0.6rem;
}

/* Label/value pairs, not a form: the profile view is read-only. */
.detail-list {
  display: grid;
  grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
  gap: 0.45rem 1rem;
  margin: 0;
  font-size: 0.875rem;
}

.detail-list dt {
  color: var(--muted);
}

.detail-list dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.member-profile-card .table-wrap table {
  min-width: min(520px, 100%);
}

/* Dues and payments both grow without limit — a brother with years of history
   pushed his own profile details off the top of the dialog and left the card
   as one very long scroll. Each list gets its own scroller instead, so the
   card keeps its shape and the two lists stay side by side in the reading
   order no matter how long either becomes. */
.member-profile-card .member-profile-section .table-wrap {
  max-height: 19rem;
}

@media (max-width: 820px) {
  .member-profile-grid,
  .member-profile-ledger {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ------------------------------------------------------- attendance view
   A meeting read rather than logged. It borrows the member profile's
   shape — identity header, figure strip, two columns, a table — because an
   officer reads the two the same way, one after the other. */
.attendance-view-card {
  width: min(100%, 900px);
  display: grid;
  gap: 1.5rem;
}

.attendance-view-card .panel-header h3 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

.attendance-view-figures {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.attendance-view-grid {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  gap: 1.5rem;
}

.attendance-view-section > .eyebrow {
  margin-bottom: 0.6rem;
}

/* Grid items default to min-width:auto, so a wide table inside a section
   would push the card past a phone's edge instead of scrolling inside its
   own .table-wrap. */
.attendance-view-card > *,
.attendance-view-section,
.attendance-view-grid > * {
  min-width: 0;
}

/* Minutes are read as written — line breaks and all — but never wider than
   the column. */
.attendance-minutes-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 22rem;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 0.85rem 1rem;
  background: var(--surface);
  font-size: 0.92rem;
  line-height: 1.55;
}

.attendance-attendees {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 22rem;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface);
}

.attendance-attendees li {
  padding: 0.5rem 0.9rem;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
}

.attendance-attendees li:first-child {
  border-top: 0;
}

.attendance-attendees li.is-you {
  font-weight: 700;
  color: var(--primary-ink);
}

.attendance-attendees li.empty-row {
  padding: 1rem;
}

.attendance-documents-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 0.6rem;
}

.attendance-documents-head > .eyebrow {
  margin: 0;
}

.attendance-documents-hint {
  margin: 0;
  font-size: 0.8rem;
}

.attendance-minutes-card {
  width: min(100%, 820px);
}

.attendance-minutes-card > * {
  min-width: 0;
}

.attendance-minutes-card textarea {
  min-height: 12rem;
  resize: vertical;
}

.attendance-view-card .table-wrap {
  max-height: 19rem;
}

/* The Mandatory mark: a word that has to be seen across a table and a list,
   so it gets the warning hue on a real plate rather than a dot. */
.status-pill.attendance-mandatory-pill {
  gap: 0;
  border-radius: var(--radius-pill);
  padding: 0.1rem 0.55rem;
  background: color-mix(in srgb, var(--warning) 18%, var(--surface));
  color: var(--warning-ink);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  vertical-align: middle;
}

.status-pill.attendance-mandatory-pill::before {
  display: none;
}

.attendance-view-card .panel-header h3 .attendance-mandatory-pill {
  font-size: 0.72rem;
}

.status-pill[data-status="Present"]::before {
  background: var(--success);
}

.status-pill[data-status="Absent"]::before {
  background: var(--danger);
}

/* The register row in the portal: the date sits over the Present/Absent mark. */
.attendance-register-item .notice-item-aside {
  display: grid;
  justify-items: end;
  gap: 0.2rem;
}

.attendance-register-item .notice-item-main strong {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
}

/* The Mandatory tick on the form reads as a sentence, not a table cell. */
.check-item.attendance-mandatory {
  align-items: flex-start;
  font-weight: 400;
}

.check-item.attendance-mandatory input {
  margin-top: 0.15rem;
}

.check-item.attendance-mandatory > span {
  display: grid;
  gap: 0.15rem;
}

.check-item.attendance-mandatory .muted {
  font-size: 0.8rem;
}

@media (max-width: 820px) {
  .attendance-view-grid,
  .attendance-view-figures {
    grid-template-columns: minmax(0, 1fr);
  }
}

.pagination-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.75rem;
  color: var(--muted);
  font-size: 0.84rem;
  font-weight: 700;
}

.pagination-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.verification-result {
  display: grid;
  gap: 0.35rem;
  margin-top: 0.85rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.85rem 1rem;
  background: var(--surface-soft);
  color: var(--muted);
  font-weight: 700;
}

.verification-result strong {
  color: var(--ink);
  font-size: 1rem;
}

.verification-result.success {
  border-color: color-mix(in srgb, var(--success) 52%, var(--line));
  background: color-mix(in srgb, var(--success) 12%, var(--surface));
}

.verification-result.danger {
  border-color: color-mix(in srgb, var(--danger) 52%, var(--line));
  background: color-mix(in srgb, var(--danger) 10%, var(--surface));
}

.page-size {
  display: flex;
  grid-auto-flow: column;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
}

.page-size select {
  width: auto;
  min-width: 72px;
  padding: 0.42rem 0.5rem;
}

.pagination-actions button {
  min-height: 34px;
  padding: 0.4rem 0.65rem;
}

.pagination-bar.single-page .pagination-actions button {
  opacity: 0.55;
}

/* The page numbers between Previous and Next: small squares, the current one
   filled so the eye finds where it is, "…" where a run of pages is folded. */
.page-numbers {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}

.pagination-actions .page-number {
  min-width: 34px;
  padding: 0.4rem 0.45rem;
  font-variant-numeric: tabular-nums;
}

.pagination-actions .page-number.is-current {
  border-color: var(--primary);
  background: var(--primary-soft, var(--surface-soft));
  color: var(--ink);
  cursor: default;
}

.page-gap {
  min-width: 1.2rem;
  text-align: center;
  color: var(--muted);
}

/* Outlined, not filled. A table of filled badges turns into confetti; a thin
   rule and the status word carry the same meaning quietly. */
/* A status is a coloured dot and a word — no outlined chip. The dot carries
   the hue at full strength for the glance; the word stays at text contrast so
   it is readable, which an all-colour chip never manages at this size. */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  max-width: 100%;
  border: 0;
  border-radius: 0;
  padding: 0;
  background: transparent;
  color: var(--ink);
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.35;
  white-space: nowrap;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  max-width: 100%;
  border: 0;
  border-radius: var(--radius-pill);
  padding: 0.25rem 0.7rem;
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 0.8rem;
  font-weight: 500;
  line-height: 1.35;
  white-space: nowrap;
}

.status-pill::before {
  content: "";
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--muted);
}

.status-pill[data-status="Active"]::before,
.status-pill[data-status="Done"]::before,
.status-pill[data-status="Paid"]::before,
.status-pill[data-status="Approved"]::before {
  background: var(--success);
}

.status-pill[data-status="Probationary"]::before,
.status-pill[data-status="In progress"]::before,
.status-pill[data-status="Pending"]::before,
.status-pill[data-status="Unpaid"]::before {
  background: var(--warning);
}

.status-pill[data-status="Inactive"]::before,
.status-pill[data-status="On hold"]::before {
  background: var(--primary-ink);
}

/* Suspended is a live problem, not an ended one — it takes the warning hue and
   the word carries weight, so it does not read as just another dot in a list. */
.status-pill[data-status="Suspended"]::before {
  background: var(--danger);
}

.status-pill[data-status="Suspended"] {
  font-weight: 600;
}

/* A declined submission is a dead end the member has to act on, so it carries
   the same weight as a suspension rather than fading out like "Inactive". */
.status-pill[data-status="Declined"]::before {
  background: var(--danger);
}

.status-pill[data-status="Declined"] {
  font-weight: 600;
}

.status-pill[data-status="Archived"]::before,
.status-pill[data-status="Rejected"]::before {
  background: var(--danger);
}

.muted {
  color: var(--muted);
  font-size: 0.84rem;
}

.member-cell {
  display: grid;
  gap: 0.15rem;
}

.member-cell strong {
  font-size: 0.92rem;
}

.profile-upload {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
}

.profile-photo {
  display: grid;
  place-items: center;
  width: 92px;
  height: 92px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-soft);
  color: var(--muted);
  font-weight: 900;
}

.profile-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * A membership card is a physical object, so it keeps its own dark plate in
 * both themes rather than following the page. It previously laid white text
 * over a translucent wash on --surface-soft, which in the light theme meant
 * white on near-white: the name was legible only because it was bold, and the
 * position, status and joined date were invisible.
 */
/*
 * A digital ID card: a credential, laid out like one.
 *
 * It used to be a dark plate with a fake QR block printed on it — the "code"
 * was a CSS checkerboard with the member number written across it, so it
 * looked scannable and was not. A card that claims a capability it does not
 * have is worse than one that does not claim it, so the block is gone and the
 * number is simply legible instead.
 *
 * The shape is a credential's: issuer along the top, portrait and name in the
 * middle, the identifying facts along the bottom. The proportions are roughly
 * a physical card's, which is what makes it read as one at a glance.
 */
.digital-id-card {
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 1.1rem;
  /* A card's size, given as one: 480 by 240. Definite, not min(100%, 480px):
     its column is sized to its contents, and a percentage has nothing to
     resolve against there — the card collapsed to whatever its text needed.
     max-width is what keeps it inside a narrower column instead. */
  width: 480px;
  max-width: 100%;
  height: 240px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  padding: 1.35rem 1.5rem 1.25rem;
  background: var(--surface-soft);
}

.digital-id-card .id-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--line);
}

/* Who issued the card. On a real one this is the line above everything. */
.digital-id-card .id-issuer {
  color: var(--ink);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.digital-id-card .id-kind {
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}

.digital-id-card .id-body {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  min-width: 0;
}

.digital-id-card .id-portrait {
  position: relative;
  flex: 0 0 84px;
  width: 84px;
  height: 84px;
}

.digital-id-card .id-photo {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border: 3px solid var(--surface);
  border-radius: 50%;
  background: var(--surface);
  color: var(--muted);
  font-size: 1.6rem;
  font-weight: 800;
}

.digital-id-card .id-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Standing, at a glance. The ring is the card's own background, so the dot
   reads as sitting on the portrait rather than floating over it. */
.digital-id-card .id-status-dot {
  position: absolute;
  right: 2px;
  bottom: 4px;
  width: 18px;
  height: 18px;
  border: 3px solid var(--surface-soft);
  border-radius: 50%;
  background: var(--muted);
}

.digital-id-card .id-portrait[data-status="Active"] .id-status-dot {
  background: var(--success);
}

.digital-id-card .id-portrait[data-status="Probationary"] .id-status-dot {
  background: var(--warning);
}

.digital-id-card .id-portrait[data-status="Suspended"] .id-status-dot {
  background: var(--danger);
}

.digital-id-card .id-identity {
  display: grid;
  gap: 0.2rem;
  min-width: 0;
}

.digital-id-card .id-name {
  font-size: 1.3rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.digital-id-card .id-meta {
  color: var(--muted);
  font-size: 0.88rem;
}

.digital-id-card .id-foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.digital-id-card .id-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0;
  min-width: 0;
}

.digital-id-card .id-facts > div {
  display: grid;
  gap: 0.1rem;
  min-width: 0;
}

.digital-id-card .id-facts dt {
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.digital-id-card .id-facts dd {
  margin: 0;
  font-size: 0.92rem;
  font-weight: 600;
}

/* The number is the point of the card, so it is the one thing set large,
   spaced and in figures that line up. */
.digital-id-card .id-fact-number dd {
  font-size: 1.08rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
}

.digital-id-card .id-actions {
  display: flex;
  gap: 0.55rem;
  flex: 0 0 auto;
}

.digital-id-card .id-action {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  min-height: 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-raised);
  cursor: pointer;
  transition:
    transform 140ms ease,
    background-color 140ms ease,
    border-color 140ms ease;
}

.digital-id-card .id-action:hover {
  border-color: var(--line-strong);
  transform: translateY(-1px);
}

.digital-id-card .id-action:active {
  transform: translateY(0);
}

.digital-id-card .id-action:focus-visible {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

.digital-id-card .id-action-icon {
  width: 1.1rem;
  height: 1.1rem;
  background: currentColor;
}

.digital-id-card .icon-copy {
  -webkit-mask: var(--icon-copy) center / contain no-repeat;
  mask: var(--icon-copy) center / contain no-repeat;
}

/*
 * A lodge's own colour or image behind the card.
 *
 * The plate is set inline, because it is data rather than design — what the
 * stylesheet owns is what happens to the lettering on top of it. Two surfaces:
 * "dark" writes in white, "light" keeps the page's ink. Everything that has a
 * colour of its own is restated for each, because a muted grey that reads well
 * on the app's surface disappears on navy.
 */
.digital-id-card[data-surface="dark"] {
  border-color: rgba(255, 255, 255, 0.14);
  color: #ffffff;
}

.digital-id-card[data-surface="dark"] .id-head,
.digital-id-card[data-surface="dark"] .id-foot {
  border-color: rgba(255, 255, 255, 0.18);
}

.digital-id-card[data-surface="dark"] .id-issuer,
.digital-id-card[data-surface="dark"] .id-name,
.digital-id-card[data-surface="dark"] .id-facts dd {
  color: #ffffff;
}

/* 7.2:1 on the default plate, and the scrim keeps an image no lighter. */
.digital-id-card[data-surface="dark"] .id-kind,
.digital-id-card[data-surface="dark"] .id-meta,
.digital-id-card[data-surface="dark"] .id-facts dt {
  color: #d8e0ec;
}

.digital-id-card[data-surface="dark"] .id-photo {
  border-color: rgba(255, 255, 255, 0.85);
  background: #ffffff;
  color: #46506080;
}

.digital-id-card[data-surface="dark"] .id-status-dot {
  border-color: rgba(255, 255, 255, 0.9);
}

/* The buttons stay white plates: on a photograph a transparent button is a
   button nobody can find. */
.digital-id-card[data-surface="dark"] .id-action {
  background: #ffffff;
  color: #101b27;
}

.digital-id-card[data-surface="light"] {
  border-color: rgba(0, 0, 0, 0.1);
  color: #101b27;
}

.digital-id-card[data-surface="light"] .id-head,
.digital-id-card[data-surface="light"] .id-foot {
  border-color: rgba(0, 0, 0, 0.12);
}

.digital-id-card[data-surface="light"] .id-issuer,
.digital-id-card[data-surface="light"] .id-name,
.digital-id-card[data-surface="light"] .id-facts dd {
  color: #101b27;
}

.digital-id-card[data-surface="light"] .id-kind,
.digital-id-card[data-surface="light"] .id-meta,
.digital-id-card[data-surface="light"] .id-facts dt {
  color: #4b5563;
}

.digital-id-card[data-surface="light"] .id-photo,
.digital-id-card[data-surface="light"] .id-action {
  background: #ffffff;
  border-color: #ffffff;
  color: #101b27;
}

.digital-id-card[data-surface="light"] .id-status-dot {
  border-color: rgba(255, 255, 255, 0.9);
}

/* The settings preview: a card-shaped swatch, so a colour is judged at the
   size it will actually be seen rather than as a chip in a picker. */
.settings-card-preview {
  display: grid;
  place-items: center;
  width: 118px;
  height: 74px;
  flex: 0 0 118px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-soft);
  color: var(--muted);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-align: center;
  text-transform: uppercase;
}

.settings-card-preview[data-surface="dark"] {
  border-color: rgba(255, 255, 255, 0.2);
  color: #ffffff;
}

.settings-card-preview[data-surface="light"] {
  color: #101b27;
}

/* A colour input still needs to look like the buttons beside it. */
.color-field {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 40px;
  padding: 0 0.75rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  background: var(--surface);
  color: var(--ink);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
}

.color-field input {
  width: 26px;
  height: 26px;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.color-field:hover {
  border-color: var(--ink);
}

.color-field:focus-within {
  outline: 2px solid var(--primary-ink);
  outline-offset: 2px;
}

/* On a narrow screen the facts and the buttons stop competing for one line. */
@media (max-width: 540px) {
  .digital-id-card {
    height: auto;
    min-height: 240px;
  }

  .digital-id-card .id-foot {
    flex-direction: column;
    align-items: stretch;
    gap: 1rem;
  }

  .digital-id-card .id-actions {
    justify-content: flex-end;
  }
}

.record-summary {
  display: grid;
  gap: 0.75rem;
}

/* A profile is read far more than it is edited, so it reads as a record: the
   same fields the panels beside it use, two to a row, with the photo and name
   leading. */
.profile-view {
  display: grid;
  gap: 1rem;
}

.profile-view-head {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

.profile-view-head strong {
  display: block;
  font-size: 1.02rem;
}

.profile-view-head .muted {
  font-size: 0.85rem;
  margin-bottom: 0.35rem;
}

.profile-view-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
  gap: 0.75rem 1.5rem;
}

/* Notes run to a paragraph, so they get the width of the panel rather than
   half of it. */
.profile-view-notes .record-field strong {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.record-field {
  display: grid;
  gap: 0.2rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--line);
}

.record-field:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.record-field span {
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 800;
  text-transform: uppercase;
}

.record-field strong {
  color: var(--ink);
  font-size: 0.95rem;
}

/*
 * The caption for a group of controls that is not one input — a checklist, a
 * file picker. A <label> cannot do this job: it points at a single control, so
 * the group gets a heading and is tied to it with aria-labelledby instead.
 * Matches the typography of a real field label so the form reads as one thing.
 */
.field-label,
.settings-logo-label {
  margin: 0;
  color: var(--ink);
  font-size: 0.85rem;
  font-weight: 600;
}

.field-label + .checklist {
  margin-top: -0.45rem;
}

.checklist {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.55rem;
  max-height: 380px;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem;
  background: var(--surface-soft);
}

.check-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 0.58rem;
  background: var(--surface);
  font-weight: 700;
}

.check-item input {
  width: 18px;
  height: 18px;
}

.progress {
  min-width: 130px;
}

.progress-track {
  height: 6px;
  overflow: hidden;
  border-radius: 999px;
  background: var(--progress-track);
}

.progress-fill {
  height: 100%;
  width: var(--progress, 0%);
  border-radius: var(--radius-pill);
  background: var(--primary-ink);
}

.progress span {
  display: block;
  margin-top: 0.25rem;
  color: var(--muted);
  font-size: 0.78rem;
}

.cashflow-bars {
  display: grid;
  gap: 0.8rem;
  margin-bottom: 1rem;
}

.bar-row {
  display: grid;
  gap: 0.35rem;
}

.bar-label {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  color: var(--ink);
  font-size: 0.88rem;
  font-weight: 500;
}

.bar-label :last-child {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.bar-track {
  height: 5px;
  overflow: hidden;
  border-radius: 999px;
  background: var(--progress-track);
}

.bar-fill {
  height: 100%;
}

.bar-fill.income {
  background: var(--success);
}

.bar-fill.expense {
  background: var(--danger);
}

.activity-list {
  display: grid;
  gap: 0.55rem;
}

.activity-item {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  border-top: 1px solid var(--line);
  padding-top: 0.55rem;
}

.activity-item strong {
  font-size: 0.88rem;
}

.activity-item span {
  color: var(--muted);
  font-size: 0.78rem;
}

/* The dashboard's two side-by-side panels read as one glance, so both are
   kept tight (user, 2026-09-13): the open-dues rows lose most of their
   vertical padding, and the activity feed is a single line per item. */
#overdueMembersBody td {
  padding-top: 0.4rem;
  padding-bottom: 0.4rem;
}

#recentActivity {
  gap: 0.3rem;
}

#recentActivity .activity-item {
  align-items: baseline;
  padding-top: 0.35rem;
}

#recentActivity .activity-item strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#recentActivity .activity-item span {
  flex: 0 0 auto;
}

.empty-row {
  padding: 1.1rem;
  color: var(--muted);
  text-align: center;
}

.file-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  overflow: hidden;
}

.file-button input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.toast {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 20;
  max-width: min(420px, calc(100vw - 2rem));
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  background: var(--surface-raised);
  box-shadow: var(--shadow);
  color: var(--ink);
  transform: translateY(120%);
  opacity: 0;
  transition:
    transform 180ms ease,
    opacity 180ms ease;
}

.toast.show {
  transform: translateY(0);
  opacity: 1;
}

/* Above the drawer breakpoint the menu is a column, and can be narrowed to
   a rail of icons by hand. */
@media (min-width: 1081px) {
  body.sidebar-compact {
    --sidebar: 82px;
  }

  body.sidebar-compact .sidebar {
    align-items: center;
    overflow-x: hidden;
    padding-inline: 0.75rem;
  }

  body.sidebar-compact .brand {
    justify-content: center;
    gap: 0;
    padding-inline: 0;
  }

  body.sidebar-compact .brand > div:not(.brand-mark) {
    display: none;
  }

  body.sidebar-compact .nav-tabs {
    width: 100%;
    padding-top: 43px;
  }

  body.sidebar-compact .dashboard-nav-row {
    flex-direction: column;
    gap: 0.3rem;
    width: 100%;
  }

  body.sidebar-compact .sidebar-control-stack {
    inset: 0 0 auto 0;
    display: flex;
    width: 100%;
    gap: 1px;
  }

  body.sidebar-compact .nav-tab {
    justify-content: center;
    gap: 0;
    padding-inline: 0.65rem;
    font-size: 0;
  }

  body.sidebar-compact .dashboard-nav-row .nav-tab {
    flex: 0 0 auto;
    width: 100%;
  }

  body.sidebar-compact .nav-tab::before {
    width: 100%;
    font-size: 1.25rem;
  }

  body.sidebar-compact .nav-tab:hover,
  body.sidebar-compact .nav-tab.active {
    transform: translateX(0);
  }
}

/*
 * Fallback counts, for the moment before the script runs and for the case where
 * it never does. They are read off the window, so they have to assume the menu
 * is taking its full width — which is why they step down earlier than the
 * strip alone would need.
 */
@media (max-width: 1840px) {
  .metric-grid {
    --metric-columns: 3;
  }
}

@media (max-width: 1340px) {
  .metric-grid.small {
    --metric-columns: 2;
  }
}

@media (max-width: 1180px) {
  .form-and-list,
  .two-column {
    grid-template-columns: 1fr;
  }

  /* Below this width the date, account label and buttons leave the greeting
     about 200px, which wraps the subtitle to three lines. Give the greeting
     its own row and let the controls sit beneath it. */
  .topbar-main {
    flex-wrap: wrap;
    row-gap: 0.85rem;
  }

  .topbar-title {
    flex: 1 1 100%;
    order: 2;
    margin-right: 0;
  }

  .topbar-meta {
    order: 3;
    margin-right: auto;
  }

  .auth-actions {
    order: 4;
  }
}

/*
 * Where the menu stops being a column and becomes a drawer.
 *
 * This used to happen at 820px, which meant that from 821px to 1180px the menu
 * held its full 20rem while the page had barely more than that left over: at
 * 830px the stat strip was 483px wide and a figure's label ran to three lines,
 * yet at 820px — ten pixels narrower — the same strip was 788px and read
 * perfectly. A layout that improves when you shrink the window is a layout with
 * the wrong breakpoint, not a layout that needs more breakpoints.
 *
 * Only the drawer mechanics move up here. The phone treatment below — larger
 * touch targets, the stacked header, single-column forms — would be silly on a
 * 1000px tablet and stays at 820px where it belongs.
 */
@media (max-width: 1080px) {
  body.mobile-nav-open {
    overflow: hidden;
  }

  .mobile-menu-toggle {
    display: inline-grid;
    order: 1;
  }

  /* The greeting takes a whole row of its own below 1180px, so the menu button
     would otherwise sit alone above it with the width of the page beside it.
     Put the account controls on that row, pushed to the far end: one band of
     furniture across the top, then the greeting under it. */
  .topbar .auth-actions {
    order: 1;
    margin-left: auto;
  }

  .mobile-nav-close {
    display: inline-grid;
  }

  .mobile-nav-backdrop {
    position: fixed;
    inset: 0;
    z-index: 38;
    border: 0;
    border-radius: 0;
    padding: 0;
    background: rgba(15, 23, 42, 0.48);
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms ease;
  }

  body.mobile-nav-open .mobile-nav-backdrop {
    display: block;
    opacity: 1;
    pointer-events: auto;
  }

  /* The close button belongs at the drawer's top corner, beside the brand.
     Left in the flow it inherited the sidebar's 2rem gap and floated in the
     dead space between the logo and the first nav item. */
  /* .nav-tabs is position: relative, so an absolutely positioned child anchors
     to the nav list, not the drawer — which is why the close button landed
     level with the first menu item. The drawer itself is the fixed element, so
     releasing .nav-tabs makes it the containing block. */
  .nav-tabs {
    position: static;
  }

  .sidebar-control-stack {
    position: absolute;
    top: 1.25rem;
    right: 1rem;
    z-index: 1;
    display: flex;
    width: auto;
    margin: 0;
  }

  .sidebar-toggle {
    display: none;
  }

  /* As a drawer over the page the sidebar has to be opaque. It inherits
     --sidebar-background, which is transparent by design: on desktop it sits
     on the sheet and needs no fill of its own. Here the page is behind it, and
     transparent meant you read the dashboard straight through the menu. */
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 40;
    width: min(320px, 86vw);
    height: 100vh;
    padding: 1.4rem 1.1rem;
    background: var(--surface);
    border-right: 1px solid var(--line);
    box-shadow: 22px 0 48px rgba(0, 0, 0, 0.22);
    transform: translateX(-104%);
    transition: transform 220ms ease;
    /* The desktop sidebar spaces brand, nav and card by 2rem. In a drawer that
       leaves a dead band under the logo. */
    gap: 1.25rem;
  }

  /* Leave room for the close button pinned in the corner above. */
  .sidebar .brand {
    padding-right: 3.25rem;
  }

  body.mobile-nav-open .sidebar {
    transform: translateX(0);
  }

  .workspace {
    margin-left: 0;
  }
}

@media (max-width: 820px) {
  .workspace {
    padding: 1.15rem 1.15rem 2.5rem;
  }

  /* The pull-out has to track the workspace's padding at every width. */
  .topbar {
    margin: -1.15rem -1.15rem 1.4rem;
    padding: 1.15rem 1.15rem 0.9rem;
  }

  /* Three stacked rows on a phone: controls, then the greeting at full width,
     then the date and account. The greeting is the point of the header, so it
     gets the whole line rather than whatever the menu button leaves behind. */
  .topbar-main {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.85rem 0.6rem;
  }

  .mobile-menu-toggle {
    order: 1;
  }

  .auth-actions {
    order: 2;
    flex: 0 0 auto;
    width: auto;
    margin-left: auto;
    justify-content: flex-end;
  }

  .topbar-title {
    order: 3;
    flex: 1 1 100%;
    margin-right: 0;
  }

  .topbar-meta {
    order: 4;
    flex: 1 1 100%;
    font-size: 0.88rem;
  }




  .nav-tabs {
    grid-template-columns: 1fr;
    gap: 0.45rem;
  }

  .nav-tab {
    min-height: 54px;
    border-radius: 8px;
    padding: 0.85rem 1rem;
    font-size: 1rem;
  }

  .nav-tab::before {
    width: 24px;
    font-size: 1.25rem;
  }

  .brand {
    align-items: flex-start;
    gap: 0.85rem;
    min-height: 88px;
    padding: 0.15rem 0.1rem 0.9rem;
  }

  .brand-mark {
    width: 52px;
    height: 52px;
  }

  .brand-mark img {
    width: 34px;
    height: 34px;
  }

  .brand h1 {
    max-width: 240px;
    font-size: 1.24rem;
    line-height: 1.22;
  }

  .topbar {
    position: sticky;
    top: 0;
    z-index: 18;
    display: grid;
    grid-template-columns: 1fr;
    align-items: stretch;
    gap: 0.65rem;
  }

  /* The ≤820px rules already lay this out as three flex rows; a grid here
     fought them and put the greeting in a narrow second column. */
  .topbar .auth-actions {
    width: auto;
  }

  .impersonation-banner {
    align-items: stretch;
  }

  .impersonation-banner button {
    flex: 0 0 auto;
  }

  /* Logout may stretch to fill the row. The theme toggle may not — it is a
     circle, and flex: 1 on every button in here is what turned it into a
     600px ellipse. */
  .topbar .auth-actions button:not(.theme-toggle) {
    flex: 1 1 auto;
  }

  .field-grid,
  .checklist {
    grid-template-columns: 1fr;
  }

  .member-toolbar {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .member-toolbar #addMemberButton {
    grid-column: 1 / -1;
  }

  .metric-grid,
  .metric-grid.small {
    --metric-columns: 2;
  }
}

@media (max-width: 560px) {
  .metric-grid,
  .metric-grid.small {
    --metric-columns: 1;
  }
}

@media (max-width: 540px) {
  .workspace {
    padding: 1rem;
  }

  .topbar {
    margin: -1rem -1rem 1.2rem;
    padding: 1rem 1rem 0.8rem;
  }

  .metric-grid,
  .metric-grid.small {
    --metric-columns: 1;
  }

  .panel-header {
    align-items: stretch;
    flex-direction: column;
  }

  .toolbar,
  .form-actions {
    align-items: stretch;
    flex-direction: column;
  }

  .member-toolbar {
    grid-template-columns: 1fr;
  }

  .pagination-bar,
  .pagination-actions {
    align-items: stretch;
    flex-direction: column;
  }

  /* Full-width buttons suit places where they stack in a column: a form's
     actions, the sign-in card, pagination. Applying it to EVERY button also
     caught the header, and `width: 100%` on a button inside a content-sized
     flex row collapses that row — which is what wrapped Logout underneath the
     theme toggle and left the toggle stranded on its own line. */
  .form-actions > button,
  .form-actions > .file-button,
  .auth-gate-actions > button,
  .pagination-actions > button {
    width: 100%;
  }

  .theme-toggle {
    flex: 0 0 38px;
    width: 38px;
    height: 38px;
    min-height: 38px;
  }
}

/* The lodge summary reuses the member-profile card shape but needs less room:
   four figures and a single row of dues, not a full ledger. */
.lodge-summary-card {
  width: min(760px, 100%);
}

/* Two by two, not four across. Four figures in a 760px dialog leaves ~170px
   each, and a peso amount beside a 46px badge does not fit in that — the
   numbers were being clipped mid-digit. */
.lodge-summary-card .metric-grid.small {
  --metric-columns: 2;
  margin-bottom: 0.5rem;
}

/* The identity-column minimum exists for member names; this table's first
   column is a count, and the reserved width read as an empty column. */
.lodge-summary-card tbody td:first-child {
  min-width: 0;
}

/* ------------------------------- invoices ------------------------------ */

/* Invoice numbers are read digit by digit and compared against paper records,
   so they get tabular figures and a monospace face: same-width digits make a
   mismatched sequence obvious at a glance. */
.invoice-no {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: var(--ink);
  font-weight: 600;
}

.settings-subhead {
  margin-top: 0.35rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}

.field-hint {
  margin: 0;
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.5;
}

.field-hint strong {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ---------------------------- platform logo ---------------------------- */

.settings-logo {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  flex-wrap: wrap;
  padding: 1rem 0 0.2rem;
}

.settings-logo-preview {
  display: grid;
  place-items: center;
  width: 76px;
  height: 76px;
  flex: 0 0 76px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  /* A checkerboard, so a transparent logo is visibly transparent rather than
     looking like it has a light grey background it will not really have. Two
     crossed gradients draw it; the tile is small enough to read as texture. */
  background-color: var(--surface);
  background-image:
    linear-gradient(45deg, var(--surface-soft) 25%, transparent 25%, transparent 75%, var(--surface-soft) 75%),
    linear-gradient(45deg, var(--surface-soft) 25%, transparent 25%, transparent 75%, var(--surface-soft) 75%);
  background-size: 12px 12px;
  background-position: 0 0, 6px 6px;
  overflow: hidden;
}

.settings-logo-preview img {
  max-width: 84%;
  max-height: 84%;
  object-fit: contain;
}

.settings-logo-actions {
  display: grid;
  gap: 0.35rem;
}

.settings-logo-actions .muted {
  margin: 0;
}

/* A table cell that opens something, without looking like a button in a row
   of amounts. */
/* Sits under the sign-in button, at the weight of an afterthought, because
   that is what it is until the day it is the only thing on the page that
   matters. */
/* Quiet, and in the corner: it is the way out of a problem rather than the
   way in, so it sits under the button at the end of the line the eye leaves
   on. Narrower than the fields, which the grid would otherwise stretch it to. */
.auth-gate-actions .auth-gate-forgot {
  justify-self: end;
  width: auto;
  min-height: 0;
  margin-top: -0.35rem;
  padding: 0.1rem 0;
  font-size: 0.78rem;
  text-align: right;
}

/* A dialog that asks one or two things does not need the width of one that
   shows a record. */
.narrow-card {
  width: min(100%, 420px);
}

.link-button {
  padding: 0;
  border: 0;
  background: none;
  color: var(--primary-ink);
  font: inherit;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.link-button:hover,
.link-button:focus-visible {
  color: var(--ink);
}

/* --------------------------- lodge administrators ---------------------- */

.lodge-admin-current {
  display: grid;
  gap: 0.5rem;
  padding-bottom: 1rem;
  margin-bottom: 0.4rem;
  border-bottom: 1px solid var(--line);
}

.lodge-admin-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
}

.lodge-admin-row:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}

/* ------------------------------- invoice ------------------------------- */

.invoice-card {
  max-width: 760px;
}

.invoice-sheet {
  display: grid;
  gap: 1.25rem;
}

.invoice-masthead {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line-strong);
}

.invoice-issuer strong {
  display: block;
  font-size: 1.05rem;
}

.invoice-number {
  text-align: right;
}

.invoice-number strong {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.15rem;
  letter-spacing: 0.02em;
}

.invoice-parties {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1.25rem;
}

.invoice-parties .eyebrow {
  margin-bottom: 0.3rem;
}

.invoice-lines {
  width: 100%;
  border-collapse: collapse;
}

.invoice-lines th,
.invoice-lines td {
  padding: 0.6rem 0.5rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
}

.invoice-lines th {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.invoice-lines .numeric {
  text-align: right;
}

.invoice-totals {
  display: grid;
  gap: 0.35rem;
  justify-items: end;
  margin-left: auto;
  min-width: 240px;
}

.invoice-total-row {
  display: flex;
  justify-content: space-between;
  gap: 2rem;
  width: 100%;
}

.invoice-total-row.due {
  padding-top: 0.5rem;
  border-top: 1px solid var(--line-strong);
  font-size: 1.05rem;
  font-weight: 700;
}

.invoice-note {
  color: var(--muted);
  font-size: 0.85rem;
}

/*
 * Printing an invoice should produce the invoice, not a screenshot of the app
 * around it. Everything but the sheet is dropped and the modal is unwound back
 * into ordinary page flow.
 */
@media print {
  body * {
    visibility: hidden;
  }

  #invoiceDialog,
  #invoiceDialog * {
    visibility: visible;
  }

  #invoiceDialog {
    position: absolute;
    inset: 0;
    display: block;
    padding: 0;
    background: #ffffff;
  }

  #invoiceDialog .invoice-card {
    max-width: none;
    box-shadow: none;
    border: 0;
  }

  .no-print {
    display: none !important;
  }
}
