/* Wrapped in @layer m3e — the layer order is declared as the first style in
   base.html: @layer properties, theme, base, m3e, components, utilities.
   This makes Tailwind utilities (@layer utilities) win over M3E component
   rules at equal specificity, which is the intended semantics. */
@layer m3e {
/* ═══════════════════════════════════════════════════════════════════════════
 * M3E Component Styles
 * Phase 3: Core Component Library
 *
 * Depends on (load first): tokens.css, typography.css, shapes.css,
 *                          motion.css, elevation.css, layout.css
 *
 * Naming: BEM with m3e- prefix (e.g. .m3e-button--filled)
 * Tokens: Only --md-sys-* custom properties. Zero hardcoded colours.
 * ═══════════════════════════════════════════════════════════════════════════ */


/* ── Shared: State Layer ────────────────────────────────────────────────── */
/*
 * Applied to all interactive components via ::before pseudo-element.
 * The container must have position:relative and overflow:hidden.
 */
.m3e-state-layer {
  position: relative;
  overflow: hidden;
}
.m3e-state-layer::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-state-layer:hover::before       { opacity: 0.08; } }
.m3e-state-layer:focus-visible::before { opacity: 0.12; }
.m3e-state-layer:active::before      { opacity: 0.16; }
.m3e-state-layer[aria-disabled="true"]::before { display: none; }


/* ═══════════════════════════════════════════════════════════════════════════
 * DIVIDER
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-divider {
  border: none;
  border-top: 1px solid var(--md-sys-color-outline-variant);
  margin: 0;
}
.m3e-divider--inset {
  margin-inline-start: var(--md-sys-spacing-4);
}
.m3e-divider--vertical {
  border-top: none;
  border-inline-start: 1px solid var(--md-sys-color-outline-variant);
  align-self: stretch;
  width: 0;
  height: auto;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * BADGE
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * ── RADII BY ROLE (Phase 2, Decision 5) ─────────────────────────────────────
 * One radius per role, written down once so the next component does not have
 * to guess. Tokens from shapes.css.
 *
 *   role                        token           px
 *   ──────────────────────────  ──────────────  ────
 *   field, badge                --radius-sm     8
 *   panel inside a card         --radius-md     12
 *   card, menu                  --radius-lg     16
 *   dialog, sheet, snackbar     --radius-xl     28
 *   pill (button, chip, FAB)    --radius-full   9999
 *
 * A badge is a status LOZENGE, not a pill: the pill shape belongs to the two
 * components a person can press (button, chip), and a badge is neither. The
 * dot and count variants stay circular anyway, because 8px is more than half
 * of their 16px box.
 * ─────────────────────────────────────────────────────────────────────────── */

.m3e-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--md-sys-shape-corner-small);
  /* A3 (Decision 5): the base tone is NEUTRAL, not error. The partial's
     default tone is neutral too, so a badge that forgets its tone is now inert
     grey rather than alarm red (D2-58: "Attendance 1" and "Waitlist 0" both
     rendered in --danger because no tone was passed). */
  background-color: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
  /* A3 (D1 System 7): every badge in the app measured 11px, the smallest type
     on a 14px surface. Base 12px, --large 13px. */
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  vertical-align: middle;
  /* A3 (D1-17, D3 System 5): a badge hugs its label and never stretches to its
     container (the "EGM" pill spanned 340px on a phone), and it never wraps
     inside its own pill (D4-06: "Treasurer approved" broke over two lines in a
     73px box). A label that does not fit gets shortened, not wrapped. */
  width: max-content;
  max-width: 100%;
  white-space: nowrap;
  /* A8-58: the base class carried no size, so a badge rendered without the
     --large modifier (which is how the drawer emits it — `m3e-badge
     m3e-badge--error`) collapsed to an 11px square and clipped its own count.
     The count is the entire point of the badge. --small still overrides. */
  height: 16px;
  min-width: 16px;
  padding: 0 4px;
  flex-shrink: 0;
}
/* break-ui P2-6: a data-driven label wider than its container spilled past
   the lozenge, because `nowrap` + `max-width` contain nothing on their own.
   A text badge that does not fit is shortened with an ellipsis (the partial
   emits the full label as `title`). `inline-block`, because `text-overflow`
   does not reach the anonymous text item of an inline-flex box; --large
   already centres its text and its line-height fills the 20px minimum.
   Block badges opt into wrapping and are left alone. `:where()` keeps this at
   one class of specificity, so a badge that asks for `inline-flex` (an icon
   beside its label) still gets it. */
.m3e-badge:where(.m3e-badge--large:not(.m3e-badge--block)) {
  display: inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Small dot badge (no count) */
.m3e-badge--small {
  width: 6px;
  height: 6px;
  min-width: 0;
  padding: 0;
}
/* Large badge (with count) */
.m3e-badge--large {
  /* L4-30: this was a FIXED `height: 16px` with no vertical padding, and it is
     what the app reaches for when a status needs words rather than a number —
     "Resolution notice not issued" on the approvals list. A multi-word label
     in a 16px box either overflowed it or was cut. A minimum, not a ceiling. */
  min-height: 20px;
  min-width: 20px;
  padding: 2px 8px;
  /* A3 (D1 System 7): 13px, one step above the 12px base. */
  font-size: 13px;
  line-height: 1.25;
  text-align: center;
}
/* A3 (D3 System 5): the rare state that genuinely needs two lines opts in.
   Everything else is single-line by contract — see `.m3e-badge`. */
.m3e-badge--block {
  width: auto;
  max-width: 32ch;
  white-space: normal;
  text-align: start;
}
.m3e-badge--primary {
  background-color: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
}
.m3e-badge--secondary {
  background-color: var(--md-sys-color-secondary);
  color: var(--md-sys-color-on-secondary);
}
/* Good standing (OD-5): m3e-badge--tertiary and m3e-badge--tertiary-container
 * render identically — the tonal "container" wash. The 118-use majority
 * already called this tone tertiary-container; the rulebook's minority
 * "solid tertiary" spelling was the error, so it is corrected here rather
 * than by touching every call site. */
.m3e-badge--tertiary,
.m3e-badge--tertiary-container {
  background-color: var(--md-sys-color-tertiary-container);
  color: var(--md-sys-color-on-tertiary-container);
}
.m3e-badge--primary-container {
  background-color: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
}
/* A3 / D2-10 — the error role has the SAME WEIGHT in both themes.
   Measured before: light was a pale wash with dark ink (`#FFDBD1` behind
   `#410002`) and dark was a fully saturated brick with pale ink (`#811D0E`),
   so the severity ramp inverted between themes — the chips that were quiet in
   light shouted in dark, and the default invoice register in dark was a 600px
   block of solid red. One declaration, one weight class: a 16% tint of the
   role hue over whatever surface it lands on. The solid error token is the
   matching ink for that custom tint; `on-error-container` is paired with the
   much stronger stock container and becomes white-on-pale in medium/high
   contrast modes. The mix is computed per theme by the browser. */
.m3e-badge--error-container {
  background-color: color-mix(in srgb, var(--md-sys-color-error) 16%, var(--md-sys-color-surface));
  color: var(--md-sys-color-error);
}
.m3e-badge--secondary-container {
  background-color: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
.m3e-badge--neutral {
  background-color: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
  /* L5-16: one step of the surface ladder is not enough separation against a
     card, so "Rejected" and "Cancelled" stopped reading as badges at all. A
     hairline gives the shape an edge without inventing a colour — and more
     states depend on neutral since L5-13/L5-29 moved onto it. */
  box-shadow: inset 0 0 0 1px var(--border);
}
/* A3: `--warn` is the rulebook's spelling and `--warning` the shipped class
   name; two roster call sites already wrote `m3e-badge--warn`, which matched
   nothing and therefore rendered in the base tone. One alias, both spellings. */
.m3e-badge--warning,
.m3e-badge--warn {
  background-color: var(--warning-container);
  color: var(--warning-on-container);
}
/* A3: solid error. The base tone is neutral now, so the alarm role has to be a
   real class. The badge partial still normalises tone="error" to
   `--error-container` (the five-role table's Error role); this class exists for
   the one place the container tone cannot be read — a badge sitting on a row
   already washed in error-container (D4-09). */
.m3e-badge--error {
  background-color: var(--md-sys-color-error);
  color: var(--md-sys-color-on-error);
}
/* D4-09 · the Overdue badge steps out of the tinted row it sits on. Badge and
   row both resolved to `error-container`, so on an overdue invoice the status
   chip stopped being a chip at all — bare coloured text on its own colour, in
   both themes. Inside a tinted row the badge inverts to the solid role. */
.m3e-table tbody tr.table-row-danger .m3e-badge--error-container,
.table-row-danger .m3e-badge--error-container {
  background-color: var(--md-sys-color-error);
  color: var(--md-sys-color-on-error);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * BUTTON
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-button {
  /* Layout */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--md-sys-spacing-2);
  /* Touch target — D5-30: the button ladder now meets the control ladder.
     A flat 48px (`--md-sys-density-touch-target-default`) used to sit a
     button 4px proud of a 44px committee control or 4px short of a 52px
     member one in the same row. The surface token used by every control
     (`--control-h-form`, colors_and_type.css) is the button's resting
     height too, so "Apply" beside a filter, or "Save" beside a text field,
     is always a flush row. `--sm` (below) is the one sanctioned exception. */
  min-height: var(--control-h-form);
  padding: 0 var(--md-sys-spacing-6);
  /* Shape */
  border-radius: var(--md-sys-shape-corner-full);
  /* Typography */
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.1px;
  line-height: 20px;
  text-decoration: none;
  white-space: nowrap;
  /* Interactive */
  cursor: pointer;
  position: relative;
  overflow: hidden;
  border: none;
  outline: none;
  /* Press is crisp: 0.97 on the effects curve, no overshoot spring — a
     dashboard button is pressed tens of times a day (review P2-9). */
  /* No box-shadow transition: buttons never change elevation (P2-15 flat
     hover), so it only cost a paint path (review P2-6). */
  transition:
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
    transform var(--md-sys-motion-duration-short2) var(--easing-effects);
}
.m3e-button:active {
  transform: scale(0.97);
}
.m3e-button:is(:disabled, [aria-disabled="true"], .m3e-button--loading):active { transform: none; }
.m3e-button::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-button:hover::before        { opacity: 0.08; } }
.m3e-button:focus-visible::before { opacity: 0.12; }
.m3e-button:active::before       { opacity: 0.16; }
.m3e-button:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* F-05 — a button never chops its own label.
 *
 * The base rule pairs `white-space: nowrap` with the `overflow: hidden` the
 * state layer needs, so any button narrower than its own text sliced the last
 * characters mid-letter with nothing to say it had: "Save communication
 * settings" is 199px of text, and `w-full` inside a 240px aside gives it a
 * 150px content box. Two rules, both cheap:
 *
 *   1. the label span truncates with an ellipsis rather than a bare cut, so
 *      the worst case is legible and obviously abbreviated. Pair it with a
 *      `title` in `attrs` where the whole label matters;
 *   2. `--wrap` (see the `wrap` param on m3e/components/button.html) lets the
 *      label take a second line and the pill grow to hold it — the right
 *      answer for a full-width button in a narrow column, where there is
 *      vertical room and the words are the point. */
.m3e-button > span:not(.m3e-button__icon):not(.m3e-button__spinner) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.m3e-button--wrap {
  height: auto;
  white-space: normal;
  padding-top: var(--md-sys-spacing-2);
  padding-bottom: var(--md-sys-spacing-2);
  text-align: center;
}
.m3e-button--wrap > span:not(.m3e-button__icon):not(.m3e-button__spinner) {
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}

/* Filled */
.m3e-button--filled {
  background-color: var(--md-sys-color-primary);
  color: var(--md-sys-color-on-primary);
  box-shadow: var(--md-sys-elevation-0);
}
/* P2-15: no hover shadow — the 8% state layer is the hover; buttons stay flat. */

/* Tonal */
.m3e-button--tonal {
  background-color: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}

/* Outlined */
.m3e-button--outlined {
  background-color: transparent;
  color: var(--md-sys-color-primary);
  border: 1px solid var(--md-sys-color-outline);
}
.m3e-button--outlined:focus-visible { border-color: var(--md-sys-color-primary); }

/* Elevated */
.m3e-button--elevated {
  background-color: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-primary);
  box-shadow: var(--md-sys-elevation-1);
}

/* Text */
.m3e-button--text {
  background-color: transparent;
  /* Court & Print budget (redesign-2026 rule 2): tertiary text actions are
   * where-you-can-go links, not the red moment. Ink + soft underline keeps
   * the court red for the page's one filled primary action; error-tone text
   * buttons still override below. */
  color: var(--fg1);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--fg1) 30%, transparent);
  text-underline-offset: 0.25rem;
  padding: 0 var(--md-sys-spacing-3);
}
@media (hover: hover) and (pointer: fine) { .m3e-button--text:hover { text-decoration-color: var(--fg1); } }

/* Error tone — overrides color for any variant */
.m3e-button--error { color: var(--md-sys-color-error); }
.m3e-button--error.m3e-button--outlined { border-color: var(--md-sys-color-error); }
.m3e-button--error.m3e-button--filled {
  background-color: var(--md-sys-color-error);
  color: var(--md-sys-color-on-error);
}
.m3e-button--error:focus-visible { outline-color: var(--md-sys-color-error); }

/* Sizes */
/* D5-30: `--sm` is the one deliberately smaller rung on the button ladder —
   36px, for a table row's action cell (a 44px row cannot host a full
   `--control-h-form` button) and the page header's compact action cluster
   (`.m3e-page-header__actions .m3e-button`, which states the same 36px
   directly rather than via this modifier). It is not a general "make this
   button smaller" escape hatch from the control ladder: everywhere else a
   `.m3e-button` rests at `--control-h-form`, same as the controls beside it. */
.m3e-button--sm {
  min-height: var(--md-sys-density-touch-target-compact);
  font-size: 12px;
  padding: 0 var(--md-sys-spacing-3);
}
.m3e-table-wrapper .m3e-button--sm {
  min-height: var(--md-sys-density-touch-target-compact);
}
/* The compact density token is 44px below 840px, but `.m3e-button--sm` is
   almost always written next to a Tailwind height utility (`h-10`, `py-2`)
   which sits in `@layer utilities` and therefore wins. State the touch floor
   as a floor so the button cannot render under 44px on a phone. */
@media (max-width: 839px) {
  .m3e-button--sm {
    min-height: 44px !important;
  }
}
.m3e-button--lg {
  min-height: 56px;
  font-size: 16px;
  padding: 0 var(--md-sys-spacing-8);
}
.m3e-button--xl {
  min-height: 64px;
  font-size: 18px;
  padding: 0 var(--md-sys-spacing-10);
}

/* Disabled */
.m3e-button[aria-disabled="true"]:not(.m3e-button--busy),
.m3e-button:disabled {
  background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent) !important;
  color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent) !important;
  border-color: transparent !important;
  box-shadow: none !important;
  cursor: not-allowed;
}
.m3e-button[aria-disabled="true"]::before,
.m3e-button:disabled::before { display: none; }

/* Loading state */
.m3e-button--loading {
  cursor: wait;
  pointer-events: none;
}
/* Busy (form-loading.js, P1-12): a submit in flight. The button keeps its
   own colours — it is working, not unavailable — and its width is locked by
   the script so the row does not reflow. The spinner and `cursor: wait`
   carry the state. When the busy label does not fit the locked width the
   label stays the accessible name but is hidden visually. */
.m3e-button--busy {
  cursor: wait;
}
.m3e-button--busy::before { display: none; }
.m3e-button--busy .m3e-loading-inline { flex-shrink: 0; }
.m3e-button__busy-label {
  overflow: hidden;
  text-overflow: ellipsis;
}
.m3e-button--busy-compact .m3e-button__busy-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
/* An auto-submitting form (hx-trigger=change) while its save is in flight. */
.npsc-form-busy {
  opacity: 0.7;
  transition: opacity var(--md-sys-motion-duration-short3) var(--easing-effects);
}
.m3e-button__icon {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
}
.m3e-button__icon svg { width: 100%; height: 100%; }

/* Loading spinner */
.m3e-button__spinner {
  width: 18px;
  height: 18px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: m3e-spin var(--md-sys-motion-duration-spinner) linear infinite;
  flex-shrink: 0;
}
/* The one ring-spinner keyframe (the screenshot reader below reuses it). */
@keyframes m3e-spin {
  to { transform: rotate(360deg); }
}

/* Disclosure trigger — full-width tonal button for expand/collapse panels */
.m3e-disclosure-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  gap: var(--md-sys-spacing-3);
  min-height: var(--md-sys-density-touch-target-default);
  padding: 0 var(--md-sys-spacing-5);
  border-radius: var(--md-sys-shape-corner-xl);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.1px;
  cursor: pointer;
  border: none;
  position: relative;
  overflow: hidden;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-disclosure-trigger::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;                 /* L7-12 */
  background: currentColor;
  opacity: 0;
  transition: opacity var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) { .m3e-disclosure-trigger:hover::before { opacity: 0.08; } }
/* Focus: the house ring (THE HOUSE FOCUS RING block) — no local outline. */
.m3e-disclosure-trigger:active { transform: scale(0.98); }
.m3e-disclosure-trigger[aria-expanded="true"] {
  background: var(--md-sys-color-surface-container);
  /* Re-pair the text/icon colour with the surface-container background. The
   * collapsed state uses secondary-container + on-secondary-container; when
   * expanded the background changes but the colour was left as
   * on-secondary-container, which fails AA in high-contrast modes (both flip so
   * the pairing breaks). on-surface is the correct pairing for surface-container. */
  color: var(--md-sys-color-on-surface);
  border-radius: var(--md-sys-shape-corner-xl) var(--md-sys-shape-corner-xl) 0 0;
}
/* Every disclosure chevron turns the same way: --duration-quick on
   --ease-rally, no overshoot (review P2-9; see .m3e-select-shell::after). */
.m3e-disclosure-trigger .toggle-chevron {
  transition: transform var(--duration-quick) var(--ease-rally);
}
.m3e-disclosure-trigger[aria-expanded="true"] .toggle-chevron {
  transform: rotate(180deg);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * DISCLOSURE  (A3 · Decision 5 — one `<details>` recipe, everywhere)
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Three `<summary>` treatments shipped before this: a 1,116×44px band with no
 * border, no fill and no chevron ("Filters" on the court-bookings ledger,
 * D2-04/D2-47); a `--surface-1` bar that read as a disabled input ("What is a
 * pattern?"); and a `m3e-title-medium` line that read as an orphaned heading
 * ("How approval works", D4-38). Two more on the governance surface shipped
 * with no marker at all (D3-64). One recipe replaces all five.
 *
 *   <details class="m3e-disclosure">
 *     <summary class="m3e-disclosure__summary">More filters</summary>
 *     <div class="m3e-disclosure__content">…</div>
 *   </details>
 *
 * The chevron is drawn by the component, so no call site has to remember an
 * icon; 44px row; a state-layer hover so it announces itself as a control; a
 * hairline under the summary once open; 8px between siblings.
 * ─────────────────────────────────────────────────────────────────────────── */

:root {
  /* Heroicons `chevron-down`, 24×24 outline — the same glyph `{% icon %}`
     emits. Held as a mask so the component can paint it in `currentColor`
     without an inline SVG at every call site. */
  --m3e-chevron-down-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m19.5 8.25-7.5 7.5-7.5-7.5'/%3E%3C/svg%3E");
}

.m3e-disclosure + .m3e-disclosure { margin-top: var(--md-sys-spacing-2); }

.m3e-disclosure__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-sys-spacing-3);
  min-height: 44px;
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-3);
  margin: 0 calc(-1 * var(--md-sys-spacing-3));
  border-radius: var(--md-sys-shape-corner-small);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.1px;
  color: var(--md-sys-color-on-surface);
  background: none;
  cursor: pointer;
  user-select: none;
  position: relative;
  list-style: none;
  transition: background-color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
/* One marker, and it is ours. `list-style: none` alone does not remove the
   triangle in Safari — `::-webkit-details-marker` does — and `::marker` with
   empty content covers Firefox. */
.m3e-disclosure__summary::-webkit-details-marker { display: none; }
.m3e-disclosure__summary::marker { content: ''; }
/* State layer: the affordance a `cursor: pointer` cannot give a phone. */
.m3e-disclosure__summary::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-disclosure__summary:hover::before        { opacity: 0.08; } }
.m3e-disclosure__summary:focus-visible::before { opacity: 0.12; }
.m3e-disclosure__summary:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
/* The chevron. A CSS mask keeps it in `currentColor` and keeps the call sites
   free of an icon include; the same Heroicons chevron-down path as `{% icon %}`. */
.m3e-disclosure__summary::after {
  content: '';
  flex: none;
  width: 20px;
  height: 20px;
  background-color: currentColor;
  -webkit-mask: var(--m3e-chevron-down-mask) center / 20px 20px no-repeat;
  mask: var(--m3e-chevron-down-mask) center / 20px 20px no-repeat;
  transition: transform var(--duration-quick) var(--ease-rally);
}
.m3e-disclosure[open] > .m3e-disclosure__summary::after { transform: rotate(180deg); }
.m3e-disclosure[open] > .m3e-disclosure__summary {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  box-shadow: 0 1px 0 0 var(--md-sys-color-outline-variant);
}
/* The rare trigger that sits inside a field slot rather than owning its own
   row ("Change venue", beside the value it changes). Same 44px target, same
   chevron, same state layer — it simply hugs its label instead of spanning. */
.m3e-disclosure__summary--inline {
  display: inline-flex;
  width: max-content;
  max-width: 100%;
  margin: 0;
}
.m3e-disclosure[open] > .m3e-disclosure__summary--inline { box-shadow: none; }
.m3e-disclosure__content { padding-top: var(--md-sys-spacing-4); }
/* The count of applied filters, or any short qualifier, sits between the label
   and the chevron and never competes with either. */
.m3e-disclosure__meta {
  margin-left: auto;
  color: var(--md-sys-color-on-surface-variant);
  font-weight: 400;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * CHECKBOX
 * ═══════════════════════════════════════════════════════════════════════════ */

/* D2-03: the belt-and-braces floor. A `border-*` class on an `appearance:auto`
   control is inert, so 42 ticked boxes in the block dialog rendered in
   Chrome's #1a73e8 directly above a "Record as held" button in `--brand`, and
   the dismiss dialog emitted bare 13×13px UA radios inside a 560px house
   dialog. The partials are still the contract — this is what any control that
   escapes them gets, so the worst case is on-brand rather than Chrome blue. */
input[type="checkbox"],
input[type="radio"] { accent-color: var(--brand); }

.m3e-checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  cursor: pointer;
  position: relative;
  /* The whole label is the target. The visual box is 20px; the label gives it
     height, and the ::after below extends the hit area around a checkbox that
     has no visible label at all (A1-15, A2-08, A3-42).
     A6a (B1 request 8 / D1-35): the box was 18px against a 16px label — the
     one control smaller than its own text. 20px box, the 12px gap this rule
     already sets, 44px row. */
  min-height: 44px;
}
/* L7-35: the old `::after` "hit-area extender" was `pointer-events: none` and
   did not change the label's box, so it extended nothing. The real 44px target
   comes from `.m3e-checkbox { min-height: 44px }` plus the phone block below.
   Deleted — `::after` is now free, and `::before` is the state layer. */
.m3e-checkbox__input {
  /* Reset and size */
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  min-width: 20px;
  border: 2px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-extra-small);
  background-color: transparent;
  cursor: pointer;
  position: relative;
  transition:
    border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-checkbox__input:checked {
  background-color: var(--md-sys-color-primary);
  border-color: var(--md-sys-color-primary);
}
.m3e-checkbox__input:checked::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 6px;
  height: 11px;
  border: 2px solid var(--md-sys-color-on-primary);
  border-top: none;
  border-left: none;
  transform-origin: center;
  transform: rotate(45deg);
}
.m3e-checkbox__input:indeterminate {
  background-color: var(--md-sys-color-primary);
  border-color: var(--md-sys-color-primary);
}
.m3e-checkbox__input:indeterminate::after {
  content: '';
  position: absolute;
  left: 2px;
  top: 50%;
  transform: translateY(-50%);
  width: 10px;
  height: 2px;
  background-color: var(--md-sys-color-on-primary);
}
.m3e-checkbox__input:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.m3e-checkbox__input:disabled {
  opacity: 0.38;
  cursor: not-allowed;
}
.m3e-checkbox__label {
  font-size: var(--control-fs);
  color: var(--md-sys-color-on-surface);
  user-select: none;
  /* The label is inside the <label>, so it is part of the target. Give it the
     full height so tapping the words works, not just the 18px box. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* The state layer (L7-04, L5-09, L2-07, L2-65).
   Was `inset: -10px` + `--primary`: a 64px blue pill painted OUTSIDE a 44px
   square box, leaving 1px of clear ground between two rows 21px apart and
   crossing the divider above and below. It now sits exactly ON the label's own
   box with the house radius, so it can never paint outside the control —
   whether the control is a bare 44x44 cell in a bulk-select column or a
   full-width row (the session hour choices, the membership declarations),
   where the whole row is the label and the whole row should light up. */
.m3e-checkbox::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-checkbox:hover::before                { opacity: 0.08; } }
.m3e-checkbox:has(:focus-visible)::before  { opacity: 0.12; }
.m3e-checkbox:active::before               { opacity: 0.16; }
.m3e-checkbox:has(:disabled)::before       { opacity: 0 !important; }


/* ═══════════════════════════════════════════════════════════════════════════
 * RADIO
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-radio {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  cursor: pointer;
  min-height: 44px;
}
.m3e-radio__input {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  min-width: 20px;
  border: 2px solid var(--md-sys-color-outline);
  border-radius: 50%;
  background-color: transparent;
  cursor: pointer;
  position: relative;
  transition:
    border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-radio__input:checked {
  border-color: var(--md-sys-color-primary);
}
.m3e-radio__input:checked::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background-color: var(--md-sys-color-primary);
}
.m3e-radio__input:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.m3e-radio__input:disabled {
  opacity: 0.38;
  cursor: not-allowed;
}
.m3e-radio__label {
  font-size: var(--control-fs);
  color: var(--md-sys-color-on-surface);
  user-select: none;
}
.m3e-radio-group {
  display: flex;
  flex-direction: column;
  gap: var(--md-sys-spacing-2);
}
/* State layer (L7-05) — the checkbox's idiom exactly: on the label's own box,
   the house radius, never outside it. A radio had no hover, focus or pressed
   state at all. */
.m3e-radio { position: relative; isolation: isolate; }
.m3e-radio::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--md-sys-shape-corner-medium);
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-radio:hover::before               { opacity: 0.08; } }
.m3e-radio:has(:focus-visible)::before { opacity: 0.12; }
.m3e-radio:active::before              { opacity: 0.16; }
.m3e-radio:has(:disabled)::before      { opacity: 0 !important; }
.m3e-radio + .m3e-radio { margin-top: var(--md-sys-spacing-2); }
/* …except inside the group, whose own `gap` already provides it. */
.m3e-radio-group > .m3e-radio + .m3e-radio { margin-top: 0; }


/* Phone touch floor for checkbox AND radio, done honestly
 * (Q1-05, Q4-08, Q6-02, Q7-10).
 *
 * Declared here, after BOTH component blocks: the radio's own 20px sizing is
 * written below the checkbox section, so an identical-specificity rule placed
 * up there lost on source order and the radio input measured 20x20.
 *
 * The previous attempt grew a 44px `::before` on the input and left the input
 * itself 18x18. That was a fiction on two counts: an overflowing pseudo-element
 * does not change the element's box, so every audit measured 18x18 and filed
 * the finding again; and it collided with the checkmark drawn by `::after`.
 *
 * The real fix: below 840px the INPUT is 44x44 — a genuine target you can
 * measure — and the 18px box people see is drawn by `::before` at the centre.
 * A negative margin pulls the extra 26px back out of the layout, so the row
 * looks exactly as it did while the hit region is the full square. Negative
 * margins move the layout box, not the border box, so the target survives.
 *
 * Every checkbox in the product must therefore carry `m3e-checkbox__input`
 * (or `m3e-radio__input`). A raw widget styled with `h-[18px] w-[18px]`
 * utilities gets neither, and — since Tailwind utilities sit above @layer m3e —
 * would beat this rule anyway. See PLATFORM_R4 §17.
 */
@media (max-width: 839px) {
  .m3e-checkbox__input,
  .m3e-radio__input {
    width: 44px;
    height: 44px;
    min-width: 44px;
    border: none;
    background-color: transparent;
    border-radius: 0;
    box-sizing: border-box;
  }
  /* A6a (D1-35): the visible box is 20px now, so the layout box the negative
     margin restores is 20px, not 18px: (44 - 20) / 2 = 12. */
  .m3e-checkbox__input { margin: -12px; }
  .m3e-radio__input { margin: -12px; }

  /* The visible control. */
  .m3e-checkbox__input::before,
  .m3e-radio__input::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    box-sizing: border-box;
    border: 2px solid var(--md-sys-color-outline);
    background-color: transparent;
    transition:
      border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
      background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
  }
  .m3e-checkbox__input::before {
    width: 20px;
    height: 20px;
    border-radius: var(--md-sys-shape-corner-extra-small);
  }
  .m3e-radio__input::before {
    width: 20px;
    height: 20px;
    border-radius: 50%;
  }
  .m3e-checkbox__input:checked,
  .m3e-checkbox__input:indeterminate {
    background-color: transparent;
    border-color: transparent;
  }
  .m3e-checkbox__input:checked::before,
  .m3e-checkbox__input:indeterminate::before {
    background-color: var(--md-sys-color-primary);
    border-color: var(--md-sys-color-primary);
  }
  .m3e-radio__input:checked { border-color: transparent; }
  .m3e-radio__input:checked::before { border-color: var(--md-sys-color-primary); }
  /* The selected radio dot: centred on the 44px box, not `inset: 3px` of it. */
  .m3e-radio__input:checked::after {
    inset: auto;
    left: 50%;
    top: 50%;
    width: 10px;
    height: 10px;
    transform: translate(-50%, -50%);
  }
  .m3e-checkbox__input:disabled::before,
  .m3e-radio__input:disabled::before { opacity: 0.38; }
  /* Ring hugs the visible box, not the 44px hit square (L7-14). The house focus
     block sets `outline-offset: … !important` on every input, and `!important`
     inside a layer beats a plain declaration in the same layer — so without the
     `!important` here the ring was drawn 2px outside the invisible 44px square,
     i.e. a ~52px ring around an 18px checkbox, painting 15px into its
     neighbours. This is the sanctioned variation the house block names. */
  .m3e-checkbox__input:focus-visible { outline-offset: -12px !important; }
  .m3e-radio__input:focus-visible    { outline-offset: -12px !important; }
  .m3e-radio { min-height: 44px; }
  /* The wrapper's gap already assumed an 18/20px control; the negative margin
     restores the geometry, so nothing else needs to change. */
}



/* ═══════════════════════════════════════════════════════════════════════════
 * SWITCH
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  cursor: pointer;
  /* The whole label is the target: 44px minimum on every viewport, and the
     visible text is inside it, so tapping the words toggles the switch. */
  min-height: 44px;
}
.m3e-switch--label-start { flex-direction: row-reverse; }
/* State layer (L7-05): the row lights up under the pointer, inside its own box,
   with the house radius. Inline switches sit tight, hence the 2px inline inset
   so two neighbouring washes keep their air (§2 law 4). */
.m3e-switch { position: relative; isolation: isolate; border-radius: var(--radius-sm); }
.m3e-switch::before {
  content: '';
  position: absolute;
  inset: 2px;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-switch:hover::before               { opacity: 0.08; } }
.m3e-switch:has(:focus-visible)::before { opacity: 0.12; }
.m3e-switch:active::before              { opacity: 0.16; }
.m3e-switch:has(:disabled)::before      { opacity: 0 !important; }
.m3e-switch + .m3e-switch { margin-top: var(--md-sys-spacing-2); }
/* Full-width settings row: label on the reading edge, track on the far edge.
   The whole row is the label, so the whole row is the target. */
.m3e-switch--block {
  display: flex;
  width: 100%;
  justify-content: space-between;
}
.m3e-switch__input {
  /* Hide the native input; state is reflected on __track */
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.m3e-switch__track {
  position: relative;
  width: 52px;
  height: 32px;
  border-radius: var(--md-sys-shape-corner-full);
  background-color: var(--md-sys-color-surface-variant);
  border: 2px solid var(--md-sys-color-outline);
  transition:
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
    border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
  flex-shrink: 0;
  cursor: pointer;
}
.m3e-switch__input:checked ~ .m3e-switch__track,
.m3e-switch__input:checked + .m3e-switch__track {
  background-color: var(--md-sys-color-primary);
  border-color: var(--md-sys-color-primary);
}
.m3e-switch__thumb {
  position: absolute;
  top: 50%;
  left: 2px;
  transform: translateY(-50%);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background-color: var(--md-sys-color-outline);
  /* Transform and colour only: the thumb is always 24px and the press grows
     it with scale(), never width/height (motion spec principle 2). Under
     reduced motion the global switch in motion.css makes it jump. */
  transition:
    transform var(--md-sys-motion-duration-short3) var(--ease-rally),
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-switch__input:checked ~ .m3e-switch__track .m3e-switch__thumb,
.m3e-switch__input:checked + .m3e-switch__track .m3e-switch__thumb {
  transform: translateY(-50%) translateX(20px);
  background-color: var(--md-sys-color-on-primary);
}
/* Press: 24px → 28px, as a scale. */
.m3e-switch__track:active .m3e-switch__thumb {
  transform: translateY(-50%) scale(1.1667);
}
.m3e-switch__input:checked ~ .m3e-switch__track:active .m3e-switch__thumb,
.m3e-switch__input:checked + .m3e-switch__track:active .m3e-switch__thumb {
  transform: translateY(-50%) translateX(20px) scale(1.1667);
}
.m3e-switch__input:focus-visible ~ .m3e-switch__track,
.m3e-switch__input:focus-visible + .m3e-switch__track {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.m3e-switch__input:disabled ~ .m3e-switch__track,
.m3e-switch__input:disabled + .m3e-switch__track {
  opacity: 0.38;
  cursor: not-allowed;
}
.m3e-switch__label {
  font-size: 14px;
  color: var(--md-sys-color-on-surface);
  user-select: none;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * PROGRESS INDICATOR
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Linear ── */
.m3e-progress--linear {
  width: 100%;
  height: 4px;
  background-color: color-mix(in srgb, var(--md-sys-color-primary) 20%, transparent);
  border-radius: var(--md-sys-shape-corner-full);
  overflow: hidden;
  position: relative;
}
.m3e-progress--linear .m3e-progress__bar {
  position: absolute;
  inset-block: 0;
  left: 0;
  background-color: var(--md-sys-color-primary);
  border-radius: inherit;
  /* No width transition: width is a layout property (motion spec principle
     2). The determinate value is server-rendered and simply lands. */
}
/* Indeterminate linear */
.m3e-progress--linear.m3e-progress--indeterminate .m3e-progress__bar {
  width: 40% !important;
  animation: m3e-linear-indeterminate var(--md-sys-motion-duration-extra-long2) linear infinite;
}
@keyframes m3e-linear-indeterminate {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(350%); }
}

/* ── Circular ── */
.m3e-progress--circular {
  display: inline-block;
  position: relative;
}
.m3e-progress--circular svg {
  transform: rotate(-90deg);
}
.m3e-progress--circular .m3e-progress__track {
  fill: none;
  stroke: color-mix(in srgb, var(--md-sys-color-primary) 20%, transparent);
}
.m3e-progress--circular .m3e-progress__arc {
  fill: none;
  stroke: var(--md-sys-color-primary);
  stroke-linecap: round;
  transition: stroke-dashoffset var(--md-sys-motion-duration-medium2)
              var(--md-sys-motion-easing-standard-effects-default);
}
/* Indeterminate circular */
.m3e-progress--circular.m3e-progress--indeterminate .m3e-progress__arc {
  animation: m3e-circular-indeterminate var(--md-sys-motion-duration-spinner-long) linear infinite;
}
@keyframes m3e-circular-indeterminate {
  0%   { stroke-dashoffset: 80; }
  50%  { stroke-dashoffset: 20; transform: rotate(135deg); }
  100% { stroke-dashoffset: 80; transform: rotate(360deg); }
}
/* Sizes */
.m3e-progress--circular.m3e-progress--sm  { width: 24px; height: 24px; }
.m3e-progress--circular.m3e-progress--md  { width: 40px; height: 40px; }
.m3e-progress--circular.m3e-progress--lg  { width: 56px; height: 56px; }

/* Color variants */
.m3e-progress--secondary .m3e-progress__bar,
.m3e-progress--secondary .m3e-progress__arc {
  stroke: var(--md-sys-color-secondary);
  background-color: var(--md-sys-color-secondary);
}
.m3e-progress--tertiary .m3e-progress__bar,
.m3e-progress--tertiary .m3e-progress__arc {
  stroke: var(--md-sys-color-tertiary);
  background-color: var(--md-sys-color-tertiary);
}

/* Circular — determinate dashoffset via CSS custom property */
.m3e-progress--circular:not(.m3e-progress--indeterminate) .m3e-progress__arc {
  stroke-dashoffset: calc(100.531 * (1 - var(--progress, 0) / 100));
}

/* ── Wavy linear — M3E expressive traveling wave ── */
.m3e-progress--wavy {
  height: 8px;
}
/* Loader exception (review P2-6): the travelling wave animates clip-path,
   which repaints. It is a constant-motion loader on one small bar, so the
   cost is accepted; reduced motion falls back to a flat bar (motion.css). */
.m3e-progress--wavy .m3e-progress__bar {
  animation: m3e-wavy-wave var(--md-sys-motion-duration-shimmer) var(--easing-effects) infinite;
}
.m3e-progress--wavy.m3e-progress--indeterminate .m3e-progress__bar {
  width: 40% !important;
  animation:
    m3e-linear-indeterminate var(--md-sys-motion-duration-extra-long2) linear infinite,
    m3e-wavy-wave var(--md-sys-motion-duration-shimmer) var(--easing-effects) infinite;
}
/* Two full sine-wave cycles across the bar; the 50% keyframe shifts the
   wave by half a wavelength so linear interpolation creates a traveling
   wave effect.  17 polygon points keep the curve smooth at small sizes. */
@keyframes m3e-wavy-wave {
  0%, 100% {
    clip-path: polygon(
      0% 50%, 6% 82%, 12% 95%, 19% 82%, 25% 50%, 31% 18%, 38% 5%, 44% 18%,
      50% 50%, 56% 82%, 62% 95%, 69% 82%, 75% 50%, 81% 18%, 88% 5%, 94% 18%,
      100% 50%, 100% 100%, 0% 100%
    );
  }
  50% {
    clip-path: polygon(
      0% 50%, 6% 18%, 12% 5%, 19% 18%, 25% 50%, 31% 82%, 38% 95%, 44% 82%,
      50% 50%, 56% 18%, 62% 5%, 69% 18%, 75% 50%, 81% 82%, 88% 95%, 94% 82%,
      100% 50%, 100% 100%, 0% 100%
    );
  }
}

/* ── M3E shape-morphing loading indicator ── */
.m3e-loading-indicator {
  width: 48px;
  height: 48px;
  background-color: var(--md-sys-color-primary);
  animation: m3e-loading-morph var(--md-sys-motion-duration-morph) var(--easing-effects) infinite;
}
.m3e-loading-indicator--secondary { background-color: var(--md-sys-color-secondary); }
.m3e-loading-indicator--tertiary  { background-color: var(--md-sys-color-tertiary); }
.m3e-loading-indicator--small { width: 24px; height: 24px; }
.m3e-loading-indicator--large { width: 64px; height: 64px; }
/* Inline variant for buttons — inherits text color, no hue shift */
.m3e-loading-inline {
  display: inline-block;
  width: 16px;
  height: 16px;
  background-color: currentColor;
  border-radius: 50%;
  animation: m3e-loading-morph var(--md-sys-motion-duration-shimmer) var(--easing-effects) infinite;
  vertical-align: middle;
}
/* 12-stop morph cycle — organic border-radius with smooth intermediate steps.
   Loader exception (review P2-6): border-radius repaints every frame. It is
   kept on purpose for the M3E shape-morph loader (one small element, only
   while something is loading); reduced motion pins it to a static circle. */
@keyframes m3e-loading-morph {
  0%      { border-radius: 50%;                                    transform: rotate(0deg)   scale(1); }
  8%      { border-radius: 45% 55% 55% 45% / 55% 45% 55% 45%;    transform: rotate(30deg)  scale(0.95); }
  16%     { border-radius: 40% 60% 60% 40% / 60% 40% 60% 40%;    transform: rotate(60deg)  scale(0.88); }
  25%     { border-radius: var(--md-sys-shape-corner-large);        transform: rotate(90deg)  scale(0.85); }
  33%     { border-radius: 55% 45% 45% 55% / 45% 55% 45% 55%;    transform: rotate(120deg) scale(0.88); }
  42%     { border-radius: 60% 40% 40% 60% / 40% 60% 40% 60%;    transform: rotate(150deg) scale(0.92); }
  50%     { border-radius: 50% 50% 50% 12%;                        transform: rotate(180deg) scale(1); }
  58%     { border-radius: 45% 55% 45% 55% / 55% 45% 55% 45%;    transform: rotate(210deg) scale(0.95); }
  66%     { border-radius: 40% 60% 50% 50% / 50% 40% 60% 50%;    transform: rotate(240deg) scale(0.85); }
  75%     { border-radius: var(--md-sys-shape-corner-medium);       transform: rotate(270deg) scale(0.85); }
  83%     { border-radius: 48% 52% 55% 45% / 52% 48% 45% 55%;    transform: rotate(300deg) scale(0.90); }
  92%     { border-radius: 50% 40% 60% 50% / 60% 50% 40% 50%;    transform: rotate(330deg) scale(0.95); }
  100%    { border-radius: 50%;                                    transform: rotate(360deg) scale(1); }
}
/* Pull-to-refresh M3E indicator (pull-refresh.js writes only the
   finger-driven position, opacity and morph progress inline). */
#pull-refresh-indicator {
  position: fixed;
  /* Under the top bar, below the notch (mobile-native MN-11). */
  top: calc(var(--shell-top-bar-height, 64px) + env(safe-area-inset-top, 0px));
  left: 50%;
  z-index: calc(var(--z-drawer) + 5);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -50px);
  transition: opacity var(--duration-quick) var(--easing-effects);
}
#pull-refresh-indicator.is-pulling { transition: none; }
.m3e-pull-indicator {
  width: 28px;
  height: 28px;
  background: var(--md-sys-color-primary);
  border-radius: 50%;
  will-change: transform;
  /* Loader exception (review P2-6): the radius morph repaints. */
  transition:
    border-radius var(--duration-med) var(--easing-effects),
    transform var(--duration-med) var(--easing-effects);
}
.m3e-pull-indicator--refreshing .m3e-pull-indicator {
  transition: none;
  animation: m3e-loading-morph var(--md-sys-motion-duration-shimmer) var(--easing-effects) infinite;
}
/* pull-refresh.js owns the pull gesture; without this Android Chrome ran its
   own native pull-to-refresh too (two spinners, then a full reload that
   dropped the htmx state). */
html { overscroll-behavior-y: contain; }


/* ═══════════════════════════════════════════════════════════════════════════
 * CHIP
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  height: 32px;
  padding: 0 var(--md-sys-spacing-4);
  border-radius: var(--md-sys-shape-corner-small);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  position: relative;
  /* Own stacking context so the state layer stays scoped to the chip. */
  isolation: isolate;
  text-decoration: none;
  transition:
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
    border-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
  user-select: none;
  border: none;
  outline: none;
  /* State layer */
}
.m3e-chip::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-chip:hover::before        { opacity: 0.08; } }
.m3e-chip:focus-visible::before { opacity: 0.12; }
.m3e-chip:active::before       { opacity: 0.16; }
/* L7-13: the `::after { inset: -8px 0 }` hit extender claimed 8px of vertical
   space the chip did not own — two chips 16px apart had TOUCHING hit regions,
   and a chip in a table row stole clicks from the rows above and below. It only
   existed above 840px, where a fine pointer makes it unnecessary; below 840px
   the chip is an honest 44px. Deleted, and the chip gets a comfortable 36px
   pointer target instead. */
@media (min-width: 840px) {
  .m3e-chip { min-height: 36px; }
}

/* Assist chip (default) */
.m3e-chip--assist {
  background-color: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline);
}

/* Elevated chip — shadow replaces outline border */
.m3e-chip--elevated {
  border-color: transparent !important;
  box-shadow: var(--md-sys-elevation-1);
}
@media (hover: hover) and (pointer: fine) { .m3e-chip--elevated:hover { box-shadow: var(--md-sys-elevation-2); } }

/* Filter chip */
.m3e-chip--filter {
  background-color: transparent;
  color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline);
}
/* Phone touch floor for EVERY chip (Q8-04, Q2-26, Q8-18).
   The `::after { inset: -8px 0 }` overhang above genuinely extends the hit
   region, but it does not change the element box — so a register row action, a
   pager chip and a league team chip all still measured 32px, and all three were
   filed again. Below 840px the chip itself is 44px tall. The `!important` is
   deliberate and scoped: call sites carry `h-8`/`py-1` utilities, and Tailwind
   utilities sit above @layer m3e. */
@media (max-width: 839px) {
  .m3e-chip {
    min-height: 44px !important;
    height: auto;
  }
}
/* C2-16 — selection is a CHIP state, not a filter-chip state.
 *
 * This rule used to name `.m3e-chip--filter` only. The finance registers' three
 * quick views ("Chase list (5)", "2026, unpaid (6)", "Largest owing (6)") are
 * `assist` chips and their template has always passed `selected=True` for the
 * active one — the class landed on the element and nothing in this file
 * answered it. So the invoice register opened filtered to the Chase list with
 * all three chips drawn identically, and the reader could not tell which list
 * they were looking at, while the identical chip row on
 * `/committee/general-meetings/` (filter chips) did show the fill. Two surfaces
 * disagreeing about what a chip row means, from one missing selector.
 *
 * Selection now paints the same way on every variant. `aria-pressed="true"`
 * (the button form) and `aria-current="true"` (the link form) are both
 * matched, so a call site that states the ARIA and forgets the class still
 * looks right. */
.m3e-chip.m3e-chip--selected,
.m3e-chip[aria-pressed="true"],
.m3e-chip[aria-current="true"],
.m3e-chip[aria-current="page"] {
  background-color: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  border-color: transparent;
}

/* Input chip */
.m3e-chip--input {
  background-color: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline);
}

/* Suggestion chip */
.m3e-chip--suggestion {
  background-color: transparent;
  color: var(--md-sys-color-on-surface-variant);
  border: 1px solid var(--md-sys-color-outline-variant);
}

/* L7-15: the chip's focus ring is inset — see the house focus block at the end
   of this file. An outset ring is sheared off by every `overflow-x-auto` chip
   rail in the app (finance settings, expenses, invoices, court bookings). */
.m3e-chip[aria-disabled="true"] {
  opacity: 0.38;
  cursor: not-allowed;
  pointer-events: none;
}
.m3e-chip__icon,
.m3e-chip__trailing-icon {
  display: flex;
  align-items: center;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}
.m3e-chip__icon svg,
.m3e-chip__trailing-icon svg { width: 100%; height: 100%; }
.m3e-chip__trailing-icon { cursor: pointer; }

/* Chip label reset — for input-variant group structure (div[role="group"]) */
.m3e-chip__label {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: inherit;
}
/* Trailing icon button inside group — reset and add own interaction */
.m3e-chip[role="group"] .m3e-chip__trailing-icon {
  background: none;
  border: none;
  padding: var(--md-sys-spacing-1);
  margin-inline-end: calc(-1 * var(--md-sys-spacing-2));
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-surface-variant);
  position: relative;
  overflow: hidden;
}
.m3e-chip[role="group"] .m3e-chip__trailing-icon::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) { .m3e-chip[role="group"] .m3e-chip__trailing-icon:hover::before { opacity: 0.08; } }
.m3e-chip[role="group"] .m3e-chip__trailing-icon:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 1px;
}

/* L7-13: the `padding-block: 8px` here was a workaround for a workaround —
   room for the `::after` hit extender that is now deleted. The chip's focus
   ring is an INSET ring (house focus block, L7-15), so a chip rail no longer
   needs vertical clearance either. */

/* Court status chip tones (court workflow redesign Task 2; D1-01/D1-36).
   FOUR status roles only — success / warning / error / neutral — and each one
   resolves to EXACTLY the same tokens as its badge counterpart, so the same
   fact reads identically whether a context calls for the 32px chip (headers,
   lists) or the 16px badge (tables, grids):

     success  -> tertiary-container      == .m3e-badge--tertiary
     warning  -> warning-container       == .m3e-badge--warning
     error    -> error-container         == .m3e-badge--error-container
     neutral  -> surface-container-high  == .m3e-badge--neutral

   There is deliberately NO `info`/blue tone: primary-container is the identity
   role (SKILL.md badge table), not a status. A blue "pending review" chip beside
   a yellow "Pending review" badge for the same fact was D1-01, the blocker this
   vocabulary exists to prevent. Do not re-add `--tone-info`. */
.m3e-chip--tone-success {
  background-color: var(--md-sys-color-tertiary-container);
  color: var(--md-sys-color-on-tertiary-container);
  border-color: transparent;
}
.m3e-chip--tone-warning {
  background-color: var(--warning-container);
  color: var(--warning-on-container);
  border-color: transparent;
}
.m3e-chip--tone-error {
  background-color: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container);
  border-color: transparent;
}
.m3e-chip--tone-neutral {
  background-color: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface-variant);
  border-color: transparent;
}
/* ── Status-chip geometry (D1-02) ──────────────────────────────────────────
   The court status chip has EXACTLY TWO heights, everywhere, in every theme
   and at every breakpoint: 32px regular and 20px compact. It previously
   rendered at 18.3 / 32 / 32.6px on one page because `--compact` was
   `height:auto` with `white-space:normal`, so a long label silently wrapped to
   two lines and the list rows went ragged. Both variants are now locked and
   truncate with an ellipsis instead of wrapping — if a label does not fit,
   shorten the label, do not let the chip grow. */
.m3e-chip--compact {
  min-height: 0 !important;
  height: 20px;
  padding: 0 8px;
  font-size: 11px;
  line-height: 1;
  white-space: nowrap;
  text-align: center;
  max-width: 100%;
}
.m3e-chip--compact::after { content: none; }
.m3e-chip__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Court-hour state pill: the label is the information; wrapping beats an
   ellipsis (spec 2026-09-09 §4). The locked-height rule above exists so a
   row of held/needed chips stays flush — this pill is the one chip whose
   whole point is the sentence it carries, and "Book now from 12pm tomorrow ·
   winners did not pay" clipped to "Book now from 12pm to…" at phone width
   told the reader nothing. Only this modifier opts out, and it must release
   the compact height too, or the second line renders outside the 20px box. */
.m3e-chip--wrap .m3e-chip__label {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.m3e-chip--wrap.m3e-chip--compact,
.m3e-chip--assist[data-hour-state].m3e-chip--wrap.m3e-chip--compact {
  height: auto;
  white-space: normal;
  padding: 3px 8px;
  line-height: 1.25;
}

/* Presentational status chips are NOT controls (D1-29). The base class hands
   every chip `cursor:pointer`, a hover state layer and an 8px `::after` hit
   extender that steals hover area from the row link underneath. A chip carrying
   `data-court-status` is rendered by _status_chip.html as a <span>: it is text,
   so it says so, and it keeps its 32px box on phones (the 44px touch floor
   below exists for tap targets — this is not one). */
.m3e-chip--assist[data-court-status] {
  cursor: default;
  user-select: text;
}
.m3e-chip--assist[data-court-status]::after,
.m3e-chip--assist[data-court-status]::before { content: none; }
@media (max-width: 839px) {
  .m3e-chip--assist[data-court-status] {
    min-height: 0 !important;
    height: 32px;
  }
  .m3e-chip--assist[data-court-status].m3e-chip--compact {
    height: 20px;
  }
  /* F-03: these two lock the phone height by attribute, which out-specifies
     `.m3e-chip--wrap.m3e-chip--compact` above — so a court-status chip asked
     to wrap wrapped its text and kept a 20px pill, and the second and third
     lines were drawn outside the coloured box. `--wrap` releases the height
     here too, exactly as it does for the hour-state pill. */
  .m3e-chip--assist[data-court-status].m3e-chip--wrap,
  .m3e-chip--assist[data-court-status].m3e-chip--wrap.m3e-chip--compact {
    height: auto;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
 * CARD
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-card {
  border-radius: var(--md-sys-shape-corner-large);
  /* L7-15 class: `overflow: hidden` clips on both axes, so any control sitting
     near a card's inner edge lost the outer pixels of its 3px @ +2px focus
     ring. `clip` still clips (a card never scrolls) but `overflow-clip-margin`
     lets the ring — and nothing else that matters — out by the 6px it needs
     from the padding box. The rounded corner still cuts the content. */
  overflow: clip;
  overflow-clip-margin: 6px;
  position: relative;
  transition:
    box-shadow var(--md-sys-motion-duration-short4) var(--easing-effects);
}
/* Elevated */
.m3e-card--elevated {
  background-color: var(--md-sys-color-surface-container-low);
  box-shadow: var(--md-sys-elevation-1);
}
@media (hover: hover) and (pointer: fine) { .m3e-card--elevated:hover { box-shadow: var(--md-sys-elevation-2); } }
/* Filled */
.m3e-card--filled {
  background-color: var(--md-sys-color-surface-container-highest);
  box-shadow: var(--md-sys-elevation-0);
}
/* Outlined */
.m3e-card--outlined {
  /* D2-10: --card-bg, not the raw token — see colors_and_type.css. Same
     value either way; the alias is what keeps this card one step above
     --page-canvas (<body>'s own background) in both themes. */
  background-color: var(--card-bg);
  border: 1px solid var(--md-sys-color-outline-variant);
  box-shadow: var(--md-sys-elevation-0);
}

/* Interactive card (link or button) */
.m3e-card[href],
.m3e-card--interactive {
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  display: block;
}
.m3e-card[href]::before,
.m3e-card--interactive::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;                 /* L7-12 */
  background: currentColor;               /* L7-11: one tint */
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-card[href]:hover::before,
.m3e-card--interactive:hover::before    { opacity: 0.08; } }
.m3e-card[href]:active::before,
.m3e-card--interactive:active::before   { opacity: 0.12; }
.m3e-card[href]:focus-visible::before,
.m3e-card--interactive:focus-visible::before { opacity: 0.12; }
.m3e-card[href]:focus-visible,
.m3e-card--interactive:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
/* A3 · Decision 5 — the hover model is the 8% ink wash and nothing else.
 *
 * This block used to lift an interactive card with
 * `transform: translateY(-3px) scale(1.005)` on an overshoot curve, with no
 * elevation change worth seeing (shadows are banned on cards), so cards
 * bounced without rising — D1 System 8. The `::before` wash above is the whole
 * hover treatment now: one property, one duration, no transform, and nothing
 * that can move a neighbouring row. `transition: all` is banned with it (it is
 * what made session rows animate properties nobody had changed). */

/* A3 · Decision 5 — a card never contains a card.
 *
 * Nesting was counted on sessions (10), league (12), notifications (8), home
 * (2), the finance reports page and the committee-case list: two and three
 * outlined containers drawing the same hairline 20px apart, so the surface
 * ladder stopped encoding depth. The templates are swept; this is the guard
 * that keeps an unswept or future nesting from re-drawing the second border.
 * An inner card shares its parent's surface instead of drawing another box. */
.m3e-card .m3e-card:not(.m3e-card--nesting-allowed) {
  background-color: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* A3 · the section rhythm that replaces the inner card: a hairline rule and
   32px of space between siblings inside one card (D2-26, D2-64, D4-28). */
.m3e-card-section + .m3e-card-section {
  margin-top: var(--md-sys-spacing-8);
  padding-top: var(--md-sys-spacing-8);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
/* A row shares the card's surface, with one hairline between siblings.
   The replacement for `.m3e-card--filled` used as a list item. */
.m3e-card-row {
  background-color: var(--card-row-bg);
  border-radius: 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  padding: var(--md-sys-spacing-4) 0;
}
.m3e-card-row:last-child { border-bottom: 0; padding-bottom: 0; }
/* The LAST VISIBLE row, not the last row in the DOM. `/member/notifications/`
   filters its queue by toggling the `hidden` attribute on rows, so under the
   default "Needs you" view the last row on screen kept drawing a hairline with
   nothing under it. A row with no visible row after it is the last one. */
.m3e-card-row[hidden] { display: none; }
.m3e-card-row:not(:has(~ .m3e-card-row:not([hidden]))) {
  border-bottom: 0;
  padding-bottom: 0;
}
.m3e-card-row:first-child { padding-top: 0; }
/* An interactive row carries the card's hover contract — the 8% wash, nothing
   else. Same treatment, no border-box of its own. */
.m3e-card-row--interactive {
  position: relative;
  cursor: pointer;
  text-decoration: none;
  color: inherit;
  display: block;
  transition: background-color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-card-row--interactive::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-card-row--interactive:hover::before        { opacity: 0.08; } }
.m3e-card-row--interactive:active::before       { opacity: 0.12; }
.m3e-card-row--interactive:focus-visible::before { opacity: 0.12; }
.m3e-card-row--interactive:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
/* A composite row may contain a primary link and a separate action. The row
   owns their shared hover wash; this link keeps only its own link and keyboard
   focus treatment so overlapping state layers cannot paint a darker rectangle. */
.m3e-card-row__primary-link {
  color: inherit;
  text-decoration: none;
}
.m3e-card-row__primary-link:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
/* A6a (B1 request 4 / D1-27): a row that is NOT itself a link but whose
   primary content is one — the session agenda row, which carries a link to the
   session and its own Book button, so it cannot be an <a> — still owes the
   reader the house hover feedback. Same 8% wash, same 120ms, NO transform: the
   largest interactive list in the product had no hover response at all while
   the two cards on the member home lifted 3px and scaled 1.005, which is two
   hover models on one surface with the busier list dead. The wash follows
   `:focus-within` too, so keyboard reading lights the same row. */
.m3e-card-row--hoverable {
  position: relative;
  isolation: isolate;
}
.m3e-card-row--hoverable::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  /* D1-27 asks for 120ms; short2 (100ms) is the step the rest of the row
     family already animates on, and one house duration beats a bespoke one. */
  transition: opacity var(--md-sys-motion-duration-short2) var(--ease-rally);
}
@media (hover: hover) and (pointer: fine) { .m3e-card-row--hoverable:hover::before { opacity: 0.08; } }
.m3e-card-row--hoverable:focus-within::before { opacity: 0.08; }

/* Card slots */
.m3e-card__media {
  width: 100%;
  aspect-ratio: 16/9;
  object-fit: cover;
  display: block;
}
.m3e-card__content {
  padding: var(--md-sys-spacing-4);
}
.m3e-card__actions {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-4) var(--md-sys-spacing-4);
}
.m3e-card__headline {
  font-size: 16px;
  font-weight: 500;
  color: var(--md-sys-color-on-surface);
  margin: 0 0 var(--md-sys-spacing-2);
}
.m3e-card__subhead {
  font-size: 14px;
  color: var(--md-sys-color-on-surface-variant);
  margin: 0 0 var(--md-sys-spacing-2);
}
.m3e-card__supporting {
  font-size: 14px;
  color: var(--md-sys-color-on-surface-variant);
  margin: 0;
}



/* ═══════════════════════════════════════════════════════════════════════════
 * LIST
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-list {
  list-style: none;
  margin: 0;
  padding: var(--md-sys-spacing-2) 0;
}
.m3e-list--containerised {
  background-color: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-medium);
  overflow: hidden;
}
.m3e-list__item {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-4);
  min-height: var(--md-sys-density-touch-target-default);
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-4);
  position: relative;
  overflow: hidden;
  color: var(--md-sys-color-on-surface);
  text-decoration: none;
  transition: background-color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-list__item[href] { cursor: pointer; }
.m3e-list__item::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;                 /* L7-12 */
  background: currentColor;               /* L7-11: one tint */
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-list__item[href]:hover::before    { opacity: 0.08; } }
.m3e-list__item[href]:focus-visible::before { opacity: 0.12; }
.m3e-list__item[href]:active::before   { opacity: 0.16; }
/* L7-16: this used to draw a SECOND ring - a plain `outline: none` cannot beat
   the house block's `!important`, so the row showed an inset ring AND an outset
   one. `.m3e-list__item[href]` is now in the sanctioned inset list at the end
   of this file, which also gives it `--m3e-focus-ring-color` instead of a
   hard-coded `--primary`. */

/* Leading slot */
.m3e-list__leading {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex-shrink: 0;
}
.m3e-list__leading-icon {
  width: 24px;
  height: 24px;
  color: var(--md-sys-color-on-surface-variant);
}
.m3e-list__avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 500;
}

/* Content */
.m3e-list__content {
  flex: 1;
  min-width: 0;
}
.m3e-list__overline {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
}
.m3e-list__headline {
  font-size: 16px;
  color: var(--md-sys-color-on-surface);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.m3e-list__supporting {
  font-size: 14px;
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
}
.m3e-list__item--two-line .m3e-list__supporting { -webkit-line-clamp: 1; }
.m3e-list__item--three-line .m3e-list__supporting { -webkit-line-clamp: 2; }

/* Trailing slot */
.m3e-list__trailing {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  flex-shrink: 0;
  color: var(--md-sys-color-on-surface-variant);
  font-size: 12px;
}
.m3e-list__trailing-icon {
  width: 24px;
  height: 24px;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * TABLE
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-table-wrapper {
  width: 100%;
  overflow-x: auto;
}
.m3e-table-wrapper--containerised {
  background-color: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-medium);
  overflow: hidden;
}
.m3e-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  color: var(--md-sys-color-on-surface);
  /* Money and counts must line up down the column. This is a house rule, not a
     per-table choice, so it lives on the component (A8-02 class). */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* ── Overflow affordance ──────────────────────────────────────────────────
 * A table that scrolls sideways with no visual cue reads as a truncated
 * table. `.m3e-table-wrapper--scrollable` is set by table-actions.js when the
 * content is wider than the wrapper; the fade and the label go away as soon as
 * the user reaches the right edge (`--scrolled-end`). */
.m3e-table-wrapper {
  position: relative;
}
/* The fade is a mask on the wrapper, not an ::after: the wrapper is the
   scroller, so an absolutely-positioned child scrolls WITH the table. After one
   swipe the old ::after gradient sat mid-table and fogged names and links
   (Kenny, 2026-09-28). Same treatment as .m3e-hscroll below. */
.m3e-table-wrapper--scrollable {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent 100%);
          mask-image: linear-gradient(to right, #000 calc(100% - 32px), transparent 100%);
}
.m3e-table-wrapper--scrolled-end {
  -webkit-mask-image: none;
          mask-image: none;
}
/* ── The shared horizontal-scroll treatment (L5-08) ───────────────────────
 * The table wrapper's fade and hint, lifted out so ANY horizontal strip can
 * have them: a chip rail, a KPI strip, a day grid. Put `data-hscroll` on the
 * scroller and table-actions.js does the rest — the same code path, so there
 * is one answer to "there is more to the right" instead of three.
 *
 * Before this, a chip rail was `flex … overflow-x-auto scrollbar-hide` with no
 * fade, no hint and no scrollbar: 839px of chips in a 294px strip and nothing
 * on screen saying so, so the expenses filter read as three chips rather than
 * eight. */
.m3e-hscroll {
  position: relative;
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
}
.m3e-hscroll::-webkit-scrollbar { display: none; }
/* The fade is a mask on the strip itself, not an ::after: a scroller's
   absolutely-positioned child scrolls WITH the content, so an ::after gradient
   would slide away with the first swipe. */
.m3e-hscroll--scrollable {
  --hscroll-fade-start: 0px;
  --hscroll-fade-end: 32px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--hscroll-fade-start),
                      #000 calc(100% - var(--hscroll-fade-end)), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--hscroll-fade-start),
                      #000 calc(100% - var(--hscroll-fade-end)), transparent 100%);
}
.m3e-hscroll--scrolled-start { --hscroll-fade-start: 32px; }
.m3e-hscroll--scrolled-end   { --hscroll-fade-end: 0px; }
.m3e-hscroll-hint {
  display: none;
  font-size: 12px;
  color: var(--md-sys-color-on-surface-variant);
  padding: var(--md-sys-spacing-1) 0 0;
}
.m3e-hscroll--scrollable + .m3e-hscroll-hint { display: block; }
.m3e-hscroll--scrolled-end + .m3e-hscroll-hint { visibility: hidden; }

.m3e-table-scroll-hint {
  display: none;
  font-size: 12px;
  color: var(--md-sys-color-on-surface-variant);
  padding: var(--md-sys-spacing-2) 0 0;
}
/* L7-18 / L4-07: these were DESCENDANT selectors while table-actions.js inserts
   the hint as the wrapper's next SIBLING, so the hint has never been visible
   anywhere in the app — and the same mismatch broke the idempotence guard, so a
   fresh <p> was appended on every scroll event. Sibling form, to match the JS. */
.m3e-table-wrapper--scrollable + .m3e-table-scroll-hint { display: block; }
.m3e-table-wrapper--scrolled-end + .m3e-table-scroll-hint { visibility: hidden; }

/* ── Phone strategy: rows become cards ────────────────────────────────────
 * Add `m3e-table--cards` to the table and `data-label="Column name"` to every
 * <td>. Below 840px each row renders as a card with the column name on the
 * reading edge and the value on the trailing edge; above it, the table is a
 * table. Six-column registers squashed to 390px are unreadable and put the
 * action column off-screen (A8-02); this is the fix, and every surface
 * workstream uses it rather than inventing its own.
 *
 *   <table class="m3e-table m3e-table--cards">
 *     <tbody><tr><td data-label="Invoice">#68</td>…</tr></tbody>
 *   </table>
 *
 * A <td> with no data-label renders full-width with no label — use that for
 * the row's action cell.
 */
@media (max-width: 839px) {
  .m3e-table--cards,
  .m3e-table--cards tbody,
  .m3e-table--cards tr,
  .m3e-table--cards td {
    display: block;
    width: auto;
  }
  .m3e-table--cards thead {
    /* Visually hidden, still read by a screen reader walking the table. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* F-04: a checkbox in that clipped head is a control nobody can see — it
     kept its place in the tab order and reported a 44x44 box sitting on the
     first card's own checkbox, so two controls answered one tap. The fix
     cannot live here: this file is `@layer m3e` and a `display` utility on the
     label is `@layer utilities`, which wins the cascade whatever the
     specificity. Each register hides its own head copy with
     `hidden min-[840px]:flex` and renders a labelled one above the cards —
     see committee_finance/invoices_index.html and expenses.html. */

  .m3e-table--cards tr {
    background: var(--md-sys-color-surface);
    border: 1px solid var(--md-sys-color-outline-variant);
    border-radius: var(--md-sys-shape-corner-medium);
    padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
    margin-bottom: var(--md-sys-spacing-3);
  }
  /* G2: `.m3e-table--cards td` and `.m3e-table td` have the SAME specificity
     (0,1,1) and the base rule is ~400 lines further down the file, so the base
     `border-bottom` won and every card-mode table drew a hairline under every
     `label : value` line — the transposed-table look the designed card exists
     to avoid. `.m3e-table.m3e-table--cards` settles it by weight rather than by
     source order, which is what the cascade should be asked here. */
  .m3e-table.m3e-table--cards td {
    border: none;
    padding: var(--md-sys-spacing-1) 0;
    text-align: start;
  }
  .m3e-table--cards td[data-label] {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--md-sys-spacing-4);
    /* G2: the recipe assumes a labelled cell holds ONE thing — the generated
       label on the reading edge, the value on the trailing edge. A cell that
       holds two or three (a name plus two badges; a file name plus its import
       date) then put them all on one flex line and pushed the last one past the
       card, off screen, with no scroller to reach it. Wrapping is the safety
       net: the worst case is a badge on a second line inside the card, never a
       value the reader cannot see. A cell that is the card's TITLE should still
       drop `data-label` entirely — see the recipe note above. */
    flex-wrap: wrap;
    row-gap: var(--md-sys-spacing-1);
  }
  .m3e-table--cards td[data-label]::before {
    content: attr(data-label);
    /* L6-32: the label is generated INSIDE the <td>, so it inherited whatever
       the column had — including the `font-mono` on the reference column, and
       the auditor's card read "Claim" in a typeface the design system does not
       own, beside four Inter labels. A label is UI, never data. */
    font-family: var(--font-ui);
    font-variant-numeric: normal;
    letter-spacing: normal;
    font-size: 12px;
    font-weight: 500;
    color: var(--md-sys-color-on-surface-variant);
    flex-shrink: 0;
  }
  .m3e-table--cards td.m3e-table__td--numeric { text-align: end; }
  /* L4-08: in card mode a row IS a rounded card, so its hover wash has to be
     the card's shape — a square full-width wash over a 12px-rounded card
     darkens the corners outside it. */
  .m3e-table--cards tbody tr.table-row-interactive {
    border-radius: var(--md-sys-shape-corner-medium);
    overflow: hidden;
  }
  /* L5-35: the same state, in card mode. A card carries its own surface, so
     the row background has to be restated on the card.
     C2-14: the 3px `border-left` that once sat on top of this was a
     side-stripe — a third signal on a row that already has the 8% tint, a
     full error border and an "Overdue" badge. Deleted; tint + badge stay. */
  .m3e-table--cards tbody tr.table-row-danger {
    background: color-mix(in srgb, var(--md-sys-color-error) 8%, transparent);
    border-color: var(--md-sys-color-error);
  }
  /* A disclosure row — a `<tr>` whose only job is to hold the expanded detail
     of the row above it (the inbound-messages log). In card mode every row is
     its own card, which would draw that detail as a SECOND card floating under
     the first with a gap between them. It is not a second record: it is the
     back of the same card, so it loses the border, the padding and the gap and
     butts against the row it belongs to. */
  .m3e-table--cards tr.m3e-table__row-detail {
    margin-top: calc(-1 * var(--md-sys-spacing-3));
    border-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    padding: 0;
  }
  /* The wrapper must stop scrolling once the rows are cards. */
  .m3e-table-wrapper:has(.m3e-table--cards) { overflow-x: visible; }
  .m3e-table-wrapper:has(.m3e-table--cards) { -webkit-mask-image: none; mask-image: none; }
  .m3e-table-wrapper:has(.m3e-table--cards) + .m3e-table-scroll-hint { display: none; }
}
/* Above the card breakpoint the detail row is an ordinary table row and takes
   the register's hairline rule, which it used to state as two Tailwind utilities
   at its own call site. */
.m3e-table__row-detail > td { border-top: 1px solid var(--border); }

/* ── `.m3e-table--cards-lg` — the same phone strategy, one breakpoint higher ──
 * `.m3e-table--cards` switches at 839px, which is the *cover* screen of an
 * unfolded Galaxy Z Fold, not its 780-860px *inner* screen — a register wide
 * enough to need cards on a phone (a sticky action column, money columns,
 * an Email/Member split) was still a real `<table>` at 860px, 1082px of
 * columns in a 512px content box, with a money cell straddling the scroll
 * fade. `.m3e-table--cards-lg` is the identical rule set at max-width:1023px
 * for a register that needs the wider allowance; this file has no
 * mixin/custom-media facility, so the block is duplicated rather than
 * shared, and the two selectors are kept together everywhere below so they
 * never drift apart again.
 *
 *   <table class="m3e-table m3e-table--cards-lg">
 *
 * table-actions.js does not need a matching change: its scrollable/hint
 * logic already reads `getComputedStyle(wrapper).overflowX !== 'visible'`
 * rather than testing for the `--cards` class name (see the F-16 note in
 * that file), so the `overflow-x: visible` rule below is all it takes for
 * the fade and the "Scroll sideways" hint to stand down in card mode.
 *
 * ── A3 / Phase 2 Decision 4 — WHEN TO REACH FOR WHICH ────────────────────
 * D4-10 measured what this transposition actually produces: one invoice as
 * nine `label : value` rows, eight hairlines and a bulk-select checkbox alone
 * in a centred row of its own, 640px tall — the column headings reprinted on
 * every card, 45 label repetitions for five invoices, 3,200px of scrolling.
 * That is the table rotated, not a card.
 *
 * So the three registers that reached for `--cards-lg` (invoices, expenses,
 * receipts) no longer transpose at all: they render a DESIGNED card,
 * `core/_partials/_register_card.html` (title / hero amount / meta / badge /
 * actions), below 1024px and the real table above it. `--cards-lg` stays for
 * a table that has no designed card yet — it is honest and it is one class —
 * but it is the fallback now, not the answer:
 *
 *   ≤ 839px   `.m3e-table--cards`      the default: any table on a phone
 *   ≤ 1023px  `.m3e-table--cards-lg`   a register too wide for the Fold's
 *                                      inner screen AND with no card partial
 *   any       `_register_card.html`    a register a person READS rather than
 *                                      scans: give it the designed card
 */
@media (max-width: 1023px) {
  .m3e-table--cards-lg,
  .m3e-table--cards-lg tbody,
  .m3e-table--cards-lg tr,
  .m3e-table--cards-lg td {
    display: block;
    width: auto;
  }
  .m3e-table--cards-lg thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .m3e-table--cards-lg tr {
    background: var(--md-sys-color-surface);
    border: 1px solid var(--md-sys-color-outline-variant);
    border-radius: var(--md-sys-shape-corner-medium);
    padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
    margin-bottom: var(--md-sys-spacing-3);
  }
  /* Same specificity fix as `--cards` above. */
  .m3e-table.m3e-table--cards-lg td {
    border: none;
    padding: var(--md-sys-spacing-1) 0;
    text-align: start;
  }
  .m3e-table--cards-lg td[data-label] {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--md-sys-spacing-4);
    /* Same safety net as `--cards` above. */
    flex-wrap: wrap;
    row-gap: var(--md-sys-spacing-1);
  }
  .m3e-table--cards-lg td[data-label]::before {
    content: attr(data-label);
    font-family: var(--font-ui);
    font-variant-numeric: normal;
    letter-spacing: normal;
    font-size: 12px;
    font-weight: 500;
    color: var(--md-sys-color-on-surface-variant);
    flex-shrink: 0;
  }
  .m3e-table--cards-lg td.m3e-table__td--numeric { text-align: end; }
  .m3e-table--cards-lg tbody tr.table-row-interactive {
    border-radius: var(--md-sys-shape-corner-medium);
    overflow: hidden;
  }
  .m3e-table--cards-lg tbody tr.table-row-danger {
    /* D4-09 / C2-14: tint plus a uniform error border. No side stripe — see
       the `--cards` rule above. */
    background: color-mix(in srgb, var(--md-sys-color-error) 8%, transparent);
    border-color: var(--md-sys-color-error);
  }
  .m3e-table-wrapper:has(.m3e-table--cards-lg) { overflow-x: visible; }
  .m3e-table-wrapper:has(.m3e-table--cards-lg) { -webkit-mask-image: none; mask-image: none; }
  .m3e-table-wrapper:has(.m3e-table--cards-lg) + .m3e-table-scroll-hint { display: none; }
}

/* ── REGISTER CARD (core/_partials/_register_card.html) ───────────────────
 * The designed replacement for the transposed table (D4-10). Title and hero
 * figure on one line, one meta line, the badge and at most one action beneath.
 * Three lines, not nine. See the partial for the contract.
 *
 * The list is a stack of rows on --surface-0 inside the register's own card —
 * NOT a card per row inside a card, which is the nesting D4-28 files. The
 * hairline between rows is the only separator; the last row loses it.
 */
.m3e-register-card-list {
  display: flex;
  flex-direction: column;
}
.m3e-register-card {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  column-gap: var(--md-sys-spacing-1);
  padding: var(--md-sys-spacing-4) 0;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.m3e-register-card:last-child { border-bottom: 0; }
/* D4-09: a leading rule, not a full-card wash. The wash made the badge stop
   being a badge (row and badge resolved to the same token) and turned the dark
   invoice register into a solid brick-red block. */
/* C2-14: this was a 3px `border-left` in `--danger`, on a row that already
   carries an "Overdue" badge. A coloured side stripe is an absolute ban in the
   loaded design skill and it was the third signal for one fact. The row takes
   the same 8% wash `.table-row-danger` gives the desktop table, so the phone
   register and the laptop register flag an overdue invoice identically: tint
   groups the rows, badge names the state. */
.m3e-register-card--danger {
  background: color-mix(in srgb, var(--md-sys-color-error) 8%, transparent);
}
/* D4-10c: the bulk-select checkbox lives in the title row, not alone in a
   centred row of its own above the card. */
.m3e-register-card__select {
  grid-column: 1;
  grid-row: 1 / span 3;
  min-height: 44px;
  min-width: 44px;
  margin-inline-start: calc(-1 * var(--md-sys-spacing-2));
}
.m3e-register-card__link {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  color: inherit;
  text-decoration: none;
  min-width: 0;
  /* A6a (B3 request 2): the link measured 264x40 on the phone register. The
     stretched `::after` below already makes the whole card clickable, but the
     link's OWN box is what a reader aims at and what the harness measures, so
     it carries the 44px floor itself. */
  min-height: 44px;
}
/* The whole card is the tap target — the link stretches over it, so a "View"
   button per row (nine of them down a phone page, D3-66) is never needed. */
.m3e-register-card__link::after {
  content: '';
  position: absolute;
  inset: 0;
}
.m3e-register-card__link:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
  border-radius: var(--md-sys-shape-corner-small);
}
.m3e-register-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--md-sys-spacing-4);
  min-width: 0;
}
.m3e-register-card__title {
  color: var(--md-sys-color-on-surface);
  font-weight: 600;
  min-width: 0;
  /* break-ui P1-5: the title is the member's name — how a treasurer tells two
     rows apart — and a one-line ellipsis cut "Aleksandra Wiśniew…" to 99px on
     a 320px phone. It wraps to two lines (the hero amount is `flex: none`, so
     it keeps its room) and only then clamps; the partial carries `title`. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.m3e-register-card__hero {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  flex: none;
  text-align: end;
}
/* The row's one number, at the money tier: label, hero figure, qualifier —
   the hierarchy D4 calls the best piece of money typography in the app. */
.m3e-register-card__amount {
  font-size: 18px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--md-sys-color-on-surface);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.m3e-register-card__hero-label { color: var(--md-sys-color-on-surface-variant); }
.m3e-register-card__meta {
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-register-card__foot {
  grid-column: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-sys-spacing-3);
  margin-top: var(--md-sys-spacing-2);
  /* Above the stretched link, so the action inside it stays clickable. */
  position: relative;
  z-index: 1;
}
/* An interactive row washes; it does not lift (Decision 5). */
@media (hover: hover) and (pointer: fine) {
  .m3e-register-card:hover {
    background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
  }
}

.m3e-table th {
  text-align: start;
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.1px;
  color: var(--md-sys-color-on-surface-variant);
  /* A6a (B3 request 3 / the list-page contract). The header row is a LABEL for
     the data, not a row of it, and it was paying a data row's block padding:
     12px above and below a 12px cap-height label, 36px of chrome before the
     first record on every register in the app. 8px still clears the 24px the
     sort control needs and buys 8px back on every list page. The inline
     padding is unchanged: the header must stay in the same column rhythm as
     the cells under it. */
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-4);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  /* L7-27: `white-space: nowrap` on every header is what pushed "Arrears
     notices" and the whole Action column past the right edge of a 1440px
     screen. A two-line header costs one row of height; an off-screen action
     column costs the treasurer the job. Numeric headers keep nowrap. */
  white-space: normal;
  text-wrap: balance;
}
.m3e-table th.m3e-table__th--numeric { text-align: end; white-space: nowrap; }

/* L7-27: opt-in sticky action column - `.m3e-table--sticky-actions` on the
   table pins the last column so "Open" survives a sideways scroll.
   The background is wrapped in `:where()` so it carries zero specificity:
   a plain selector at 0-2-1 beat every row/cell background a template could
   put on the pinned cell (e.g. `.table-row-danger`'s error-container tint),
   painting a pale stripe over a tinted row (Wave D finding, invoice
   register). At zero specificity a row or cell background now wins; this
   declaration only supplies the default when nothing more specific is set. */
:where(.m3e-table--sticky-actions) :where(th, td):last-child {
  position: sticky;
  right: 0;
  background-color: var(--md-sys-color-surface);
}
.m3e-table__sort-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-1);
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  padding: 0;
  /* Own a stacking context so the negative-z hit-area extender stays behind. */
  position: relative;
  z-index: 0;
}
/* Invisible ≥44px hit area over the compact sort control (WCAG 2.5.5). The
   header text is short, so extend generously without shifting layout. */
.m3e-table__sort-btn::after {
  content: '';
  position: absolute;
  inset: -14px -12px;
  min-height: 44px;
  z-index: -1;
}
@media (hover: hover) and (pointer: fine) { .m3e-table__sort-btn:hover { color: var(--md-sys-color-on-surface); } }
.m3e-table__sort-icon {
  width: 16px;
  height: 16px;
  opacity: 0.4;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-table__sort-btn[aria-sort] .m3e-table__sort-icon { opacity: 1; }
.m3e-table td {
  padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  vertical-align: middle;
}
.m3e-table td.m3e-table__td--numeric { text-align: end; }
.m3e-table tbody tr {
  transition: background-color var(--md-sys-motion-duration-short1)
              var(--md-sys-motion-easing-standard-effects-default);
}
/* L7-07 / L5-10: the blanket row hover is gone. A wash means "this does
   something" — on the audit log and every auditor register the rows are
   read-only records, so the row lit up, the cursor stayed an arrow, and the
   click did nothing. Only a row that navigates lights up now, and it says so
   with its cursor. Templates opt in with `table-row-interactive`. */
.m3e-table tbody tr.table-row-interactive { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  .m3e-table tbody tr.table-row-interactive:hover {
    background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
  }
}
.m3e-table tbody tr:focus-within { box-shadow: var(--m3e-focus-ring-inset); }
.m3e-table tbody tr:last-child td { border-bottom: none; }
.m3e-table__empty {
  text-align: center;
  padding: var(--md-sys-spacing-8);
  color: var(--md-sys-color-on-surface-variant);
  font-style: italic;
}

/* A3 / Decision 4 — the constant-column line above a table
   (core/_partials/_table_constants.html). It is a caption, not a heading: the
   facts it carries were columns a moment ago, so it sits quietly above the
   header row in the same ink the header uses. */
.m3e-table-constants {
  margin: 0 0 var(--md-sys-spacing-3);
  color: var(--md-sys-color-on-surface-variant);
}
.m3e-table-constants__label {
  font-weight: 500;
  color: var(--md-sys-color-on-surface);
}

/* A3 / D4-25 — a `<thead>` is furniture for data that does not exist.
 *
 * Measured: `/committee/finance/reports/` printed "Generated / Document /
 * Period / Signatures / Actions" above nothing, then a 390px illustrated empty
 * state; the same pattern shipped verbatim on year-end, opening balances, the
 * invoice page and member billing. Five column names above zero rows tell the
 * reader what they are missing, which is the opposite of what an empty state
 * is for.
 *
 * A single row whose one cell spans every column is by construction not data —
 * that is how every empty state in the app is rendered ("{% empty %}" → one
 * `<tr>` with a `<td colspan="n">"). Templates that need to be explicit (a
 * genuine one-row table with a spanning cell) opt out by NOT being alone, or
 * opt in early with `.m3e-table__empty-row` on the row itself. */
.m3e-table:has(> tbody > tr:only-child > td[colspan]) > thead,
.m3e-table:has(> tbody > tr.m3e-table__empty-row) > thead {
  display: none;
}

/* Striped variant */
.m3e-table--striped tbody tr:nth-child(even) {
  background-color: color-mix(in srgb, var(--md-sys-color-surface-container-low) 50%, transparent);
}

/* Dense variant */
.m3e-table--dense th,
.m3e-table--dense td {
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-3);
}

/* Sticky header. The wrapper's `overflow-x: auto` makes the wrapper itself the
 * scroll container, so `top` is relative to the wrapper's edge — any offset
 * (e.g. compensating for the fixed app bar) would permanently displace the
 * header downward and let rows paint above it. Keep it at 0: the header
 * engages when the wrapper scrolls vertically (see --vscroll below); on
 * window-scrolled pages the wrapper never scrolls and the header simply
 * scrolls with the page. */
.m3e-table--sticky-header th {
  position: sticky;
  top: 0;
  background-color: var(--md-sys-color-surface-container-high);
  z-index: 1;
}

/* Wrapper variant for tables capped with a max-height in the template: makes
 * the wrapper clip and scroll vertically (a bare max-height without this lets
 * rows paint past the container), which is what lets the sticky header above
 * actually park at the wrapper's top edge while rows scroll beneath it. */
.m3e-table-wrapper--vscroll {
  overflow-y: auto;
}

/* Containerised table (elevated wrapper) */
.m3e-table-wrapper--elevated {
  background-color: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-medium);
  box-shadow: var(--md-sys-elevation-1);
  overflow: hidden;
}

/* Row actions: quiet until the row is hovered or holds keyboard focus. Never
 * display:none, so the controls stay in tab order and are always focusable;
 * focusing one reveals the cluster. Touch devices (no hover) show them always. */
.m3e-row-actions {
  opacity: 0;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-table tbody tr:hover .m3e-row-actions,
.m3e-table tbody tr:focus-within .m3e-row-actions {
  opacity: 1;
}
@media (hover: none) {
  .m3e-row-actions { opacity: 1; }
}

/* Compact inline form control for dense table / card row actions. Token-driven
 * so dark mode and contrast follow automatically; keyboard focus is always visible. */
.m3e-inline-control {
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-3);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface);
  color: var(--md-sys-color-on-surface);
  font: inherit;
}
.m3e-inline-control:focus-visible {
  outline: none;
  border-color: var(--md-sys-color-primary);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--md-sys-color-primary) 40%, transparent);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * THE HOUSE CONTROL BOX (D5 §S1 — one control family)
 *
 * Every single-line control in the product is this box: one height per
 * SURFACE (`--control-h-form`), `--control-radius`, 1px `--control-border`,
 * `--control-fill`, `--control-ink`, `--control-fs`. The tokens live in
 * colors_and_type.css; nothing below restates a number.
 *
 * Bare element selectors are deliberate. A `<select>`, a `<textarea>` or a
 * native date field that slips into a template without a class still lands in
 * the family — the audit found 58 raw `<select>` in 32 templates across ≥12
 * class recipes, and a system that only works when the author remembers the
 * class is not a system.
 *
 * If you are about to write a height, radius, border or fill on a control:
 * don't. Change the token, or you are building the seventh control family.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-control,
.m3e-text-field--outlined .m3e-text-field__input-wrapper,
.m3e-select,
.m3e-select-field select,
select,
textarea,
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"],
input[type="search"]:not(.m3e-search-bar__input):not(.m3e-nav-drawer__quick-find),
.ts-wrapper.single {
  min-height: var(--control-h-form);
  padding-inline: var(--control-pad-x);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--control-fill);
  color: var(--control-ink);
  font-family: inherit;
  font-size: var(--control-fs);
  line-height: 1.4;
  transition: border-color var(--duration-quick) var(--ease-rally);
}
/* An input is a replaced-ish box: it centres its own text against min-height,
   so it takes no vertical padding. A textarea does. */
.m3e-control,
select,
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"],
input[type="search"]:not(.m3e-search-bar__input):not(.m3e-nav-drawer__quick-find) {
  padding-block: 0;
}
textarea,
textarea.m3e-control {
  padding-block: 10px;
  min-height: var(--textarea-h, var(--textarea-h-sm));
  resize: vertical;
  line-height: 1.5;
}
select { cursor: pointer; }

/* D5-11: ONE fill. A transparent control on `--surface-1` reads as a hole in
   the card while the select beside it reads as a chip on top of it — two
   mental models of what a field is, side by side. `--surface-0` is the only
   choice that survives the whole card ladder. */

/* ── Money (D4-18) ─────────────────────────────────────────────────────────
 * Every money OUTPUT on the finance surface is `SGD 120.00`, right-aligned and
 * tabular. Every money INPUT was a bare left-aligned number box as wide as the
 * form. One class makes the two agree. */
.m3e-money-field {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  min-height: var(--control-h-form);
  padding-inline: var(--control-pad-x);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--control-fill);
  max-width: 12rem;
  width: 100%;
  transition: border-color var(--duration-quick) var(--ease-rally);
}
.m3e-money-field:focus-within { border-color: var(--brand); }
/* The input inside a standalone money field draws no second box. */
.m3e-money-field > input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  outline: none;
}
.m3e-text-field--money { max-width: 12rem; }
.m3e-money-field__affix {
  flex: none;
  font-size: var(--control-fs);
  color: var(--fg2);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  user-select: none;
}
.m3e-money-field input,
.m3e-text-field--money input,
input.m3e-money-field {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* Inside a table cell or a formset row the field is already sized by the
   column; the affix and the alignment are the point, not the cap. */
.m3e-table .m3e-text-field--money,
.m3e-money-field--wide { max-width: none; }
/* Number inputs are typed, not stepped, everywhere in this product. */
input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { appearance: none; margin: 0; }
.npsc-num-right { text-align: right; font-variant-numeric: tabular-nums; }


/* ═══════════════════════════════════════════════════════════════════════════
 * TEXT FIELD
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-text-field {
  display: flex;
  flex-direction: column;
  gap: var(--control-gap);
  position: relative;
}

/* ── Outlined variant ── */
.m3e-text-field--outlined .m3e-text-field__input-wrapper {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
}
/* D5-08: the border width NEVER changes on a state. The old rule went 1→2px on
   `:focus-within`, which added 2px of outer height to a `min-height` box — so
   tabbing down a form nudged everything below the focused field by 2px on
   every step, and the field's own text shifted 1px left. Colour only; the
   house 3px ring does the rest. */
.m3e-text-field--outlined .m3e-text-field__input-wrapper:focus-within {
  border-color: var(--brand);
  box-shadow: none;
}

/* ── Filled variant ── */
.m3e-text-field--filled .m3e-text-field__input-wrapper {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  background-color: var(--md-sys-color-surface-container-highest);
  border-radius: var(--control-radius) var(--control-radius) 0 0;
  border: 0;
  border-bottom: 1px solid var(--control-border);
  padding-inline: var(--control-pad-x);
  min-height: var(--control-h-form);
  transition: border-color var(--duration-quick) var(--ease-rally);
}
.m3e-text-field--filled .m3e-text-field__input-wrapper:focus-within {
  border-bottom-color: var(--brand);
}
.m3e-text-field--filled.m3e-text-field--error .m3e-text-field__input-wrapper {
  border-bottom-color: var(--danger);
}

/* A textarea stretches the wrapper rather than being centred in it. */
.m3e-text-field__input-wrapper:has(> textarea) {
  align-items: stretch;
  min-height: var(--textarea-h, var(--textarea-h-sm));
}
.m3e-text-field--textarea-md { --textarea-h: var(--textarea-h-md); }

/* ── Input override: the wrapper carries the box, the control carries none ──
 * The control inside a text field must never draw a second box. This also
 * neutralises any residual utility a template put on the widget. */
.m3e-text-field__input-wrapper > input,
.m3e-text-field__input-wrapper > textarea,
.m3e-text-field__input-wrapper > select {
  border: none !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  outline: none !important;
  padding: 0 !important;
  min-height: 0 !important;
  width: 100% !important;
  flex: 1;
  min-width: 0;
  color: var(--control-ink) !important;
  font-size: var(--control-fs) !important;
  font-family: inherit !important;
  ring-offset-width: 0 !important;
  /* Override Tailwind focus ring */
  --tw-ring-shadow: none !important;
  --tw-ring-offset-shadow: none !important;
}
/* D5-02: `background` (the SHORTHAND) also reset `background-image`, which
   erased the chevron on any <select> that slipped inside a text field —
   three of the four selects on the expense form rendered as filled text
   boxes with 40px of reserved but empty right padding and no affordance of
   any kind. `background-color` is the property that was meant. */
.m3e-text-field__input-wrapper > input,
.m3e-text-field__input-wrapper > textarea,
.m3e-text-field__input-wrapper > select {
  background-color: transparent !important;
}
.m3e-text-field__input-wrapper > select {
  cursor: pointer;
  padding-right: calc(var(--control-glyph) + var(--md-sys-spacing-2)) !important;
}
/* A6a (B2 request 9): the inner control fills its wrapper, so the whole
   44/52px box is the tap target.

   The control used to be `min-height: 0`, which left the real target of every
   house date, time and number field in the app at the ~22px line box of the
   <input> inside a 44-52px wrapper — and the wrapper is a <div>, so tapping
   the other 22px did nothing at all.

   `inherit` takes the wrapper's own computed min-height. The 1px negative
   block margin is what keeps that from ADDING height: the wrapper is a 44px
   BORDER box with a 1px edge, so its interior is 42px, and a 44px child with
   no margin pushes the whole field to 46px — which is A1's "no two controls in
   one row may differ in height" contract broken on five forms (measured: 24
   findings, every one of them a date field 2px taller than the select beside
   it). With -1px above and below, the control's own 44px box sits OVER the
   wrapper's border rather than on top of it: the field stays exactly
   `--control-h-form`, nothing moves, and the target is the whole box.

   Textareas are excluded: they stretch the wrapper by design and set their own
   min-height below. Checkboxes and radios never live in a text-field wrapper,
   and must keep their own 20px geometry if one ever does. */
.m3e-text-field__input-wrapper > input:not([type="checkbox"]):not([type="radio"]),
.m3e-text-field__input-wrapper > select {
  min-height: inherit !important;
  margin-block: -1px;
}
/* When the wrapper holds an absolutely-positioned trailing control (e.g. the
   password-visibility toggle, a 44px button), reserve room so the input text
   never slides underneath it. */
.m3e-text-field__input-wrapper:has(> button) > input {
  padding-right: 44px !important;
}
.m3e-text-field__input-wrapper > textarea {
  resize: vertical;
  padding-block: 10px !important;
  min-height: calc(var(--textarea-h, var(--textarea-h-sm)) - 2px) !important;
  line-height: 1.5;
}

/* Icons */
.m3e-text-field__icon {
  display: flex;
  align-items: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  color: var(--md-sys-color-on-surface-variant);
}
.m3e-text-field__icon svg { width: 100%; height: 100%; }
.m3e-text-field--error .m3e-text-field__icon--trailing {
  color: var(--md-sys-color-error);
}

/* ── Drop zone (D5-15 / D3-23) ─────────────────────────────────────────────
 * `m3e/components/drop_zone.html`. Before this the app's only file affordance
 * was a Tailwind string copied five times, with the app's fourth radius
 * (12px), its only dashed 2px border and its only `--border` control edge
 * (2.08:1 in dark), at two arbitrary heights — plus one naked
 * `<input type=file>` rendering the raw Chromium "Choose File  No file
 * chosen" in the OS font inside a 973px house box. */
.m3e-drop-zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--md-sys-spacing-1);
  min-height: 112px;
  padding: var(--md-sys-spacing-4);
  text-align: center;
  cursor: pointer;
  border: 1px dashed var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--surface-1);
  color: var(--fg1);
  transition:
    border-color var(--duration-quick) var(--ease-rally),
    background-color var(--duration-quick) var(--ease-rally);
}
.m3e-drop-zone__icon {
  width: 24px;
  height: 24px;
  color: var(--fg2);
}
.m3e-drop-zone__action { font-size: 14px; font-weight: 500; color: var(--brand); }
.m3e-drop-zone__limit  { font-size: 12px; color: var(--fg2); }
@media (hover: hover) and (pointer: fine) {
  .m3e-drop-zone:hover { border-color: var(--brand); background-color: var(--brand-container); }
}
.m3e-drop-zone.is-dragover {
  border-color: var(--brand);
  background-color: var(--brand-container);
}
.m3e-drop-zone:has(:disabled) { opacity: 0.38; cursor: not-allowed; }
/* The chosen file, as a 44px row — none of the five hand-rolled zones showed
   what had been chosen. */
.m3e-drop-zone__chip {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  min-height: 44px;
  margin-top: var(--control-gap);
  padding: 0 var(--control-pad-x);
  border: 1px solid var(--border);
  border-radius: var(--control-radius);
  background-color: var(--surface-0);
  font-size: 14px;
  color: var(--fg1);
}
.m3e-drop-zone__chip-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-drop-zone__chip-size { flex: none; color: var(--fg2); font-variant-numeric: tabular-nums; }

/* Drop-zone keyboard focus ring. The templates add `.m3e-drop-zone` to the
   label that wraps a visually-hidden (`sr-only`) file input; a keyboard user
   focusing that hidden input gets no visible affordance on the label without
   this. Both selectors cover it: :focus-within for the general case, and the
   :has(:focus-visible) form to keep the ring keyboard-only where supported.
   The geometry is the house ring's — three px at two px, like every other
   control (it used to be a 2px outline at a 12px radius: a fifth treatment). */
.m3e-drop-zone:focus-within,
.m3e-drop-zone:has(> input.sr-only:focus-visible),
.m3e-drop-zone:has(> input:focus-visible),
/* …and where the input is the label's NEXT SIBLING rather than its child
   (the block-confirm dialog's "Operator invoice" zone), in which case neither
   :focus-within nor the descendant :has() above can ever match — 2026-09-11
   re-audit: the zone took focus with no visible ring at all. */
.m3e-drop-zone:has(+ input:focus-visible) {
  outline: var(--m3e-focus-ring-width) solid var(--m3e-focus-ring-color);
  outline-offset: var(--m3e-focus-ring-offset);
}

/* iOS/iPadOS zoom the viewport when a focused text-entry control has a
   font-size below 16px. Enforce a 16px floor on text-entry controls at
   mobile/tablet widths. The standard M3E inputs already use text-base (16px)
   on mobile, so this only lifts sub-16px controls (raw inputs, compact grid
   cells) — the zoom fix, not a visual restyle. Checkbox/radio/file/range/color
   are excluded: they never zoom and carry no meaningful font-size. */
@media (max-width: 839px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]),
  select,
  textarea {
    font-size: 16px;
  }
}

/* ── Label / help / error anatomy (D5 §S6) ─────────────────────────────────
 * ONE field label, ONE rhythm, everywhere:
 *
 *   [label]    .m3e-field-label   12px / 500 / 0.4px / --fg2, 4px below
 *   [control]  the house box
 *   [help|error] 12px, --fg2 | --danger, 4px above, flush with the control's
 *                own text column
 *
 * The audit measured ELEVEN label treatments and FOUR label→control gaps —
 * the partial at 12px/500/normal tracking/2px, `m3e-label-medium` at the same
 * size with 0.5px tracking and a 0, 4 or 12px gap, `m3e-label-large` at 14px,
 * and 39 templates with no colour class at all so the label came out in --fg1.
 * `.m3e-field-label` is the one class; `.m3e-text-field__label` is its alias
 * so the partials' existing markup lands on the same values. */
/* The generic field column: label, control, help. `.m3e-text-field` and
   `.m3e-select-field` are the two named cases of it. */
.m3e-field {
  display: flex;
  flex-direction: column;
  gap: var(--control-gap);
}
/* A row of fields keeps one baseline even when one label wraps to two lines
   and only one of them has help text — three controls that should read as one
   row used to read as a staircase (D5-19). */
.m3e-field-row {
  display: grid;
  gap: var(--md-sys-spacing-4);
  align-items: start;
}
.m3e-field-row .m3e-field-label,
.m3e-field-row .m3e-text-field__label { min-height: 2lh; }

.m3e-field-label,
.m3e-text-field__label,
.m3e-select-field .m3e-text-field__label {
  display: block;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.33;
  letter-spacing: 0.4px;
  text-transform: none;
  color: var(--fg2);
  margin-bottom: var(--control-gap);
  transition: color var(--duration-quick) var(--ease-rally);
}
/* A group legend is deliberately a tier up, everywhere — not by accident on
   two forms. */
.m3e-field-legend {
  display: block;
  font-size: var(--md-sys-typescale-title-small-size, 14px);
  font-weight: 500;
  color: var(--fg1);
  margin-bottom: var(--md-sys-spacing-2);
}
/* "Optional details" — where a form has a RUN of optional fields, they are
   grouped under one heading and the per-field marker is switched off, instead
   of repeating the same grey word eighteen times down one column (D3-15). */
.m3e-optional-group > .m3e-field-legend { color: var(--fg2); }

/* Helper / error / hint text — flush with the control's own text column.
   It used to be indented 16px under controls padded 12px: it lined up under a
   text field, sat 4px right of the value under a select, and ran flush left
   under a checkbox. Three indents for one kind of text. */
.m3e-text-field__helper-text,
.m3e-field-help {
  font-size: 12px;
  line-height: 1.4;
  color: var(--fg2);
  padding-inline: var(--control-pad-x);
  margin-top: var(--control-gap);
}
.m3e-text-field__error-text,
.m3e-field-error {
  font-size: 12px;
  line-height: 1.4;
  color: var(--danger);
  padding-inline: var(--control-pad-x);
  margin-top: var(--control-gap);
}
/* A6a (B1 request 5 / D1-01's second clause): reserve the error line's height,
   so the FIRST genuine error a member makes does not shove the submit button
   down the page as they tab out of the field.

   The reservation is present exactly when the message is ABSENT and is
   released the moment it arrives, so the field's total height never changes —
   which is cheaper than an always-on 20px slot under every field in the app
   (that would cost ~24px x every control on every form, and the same audit is
   fighting for vertical space everywhere else).

   Scoped to `form[data-validate]`, the only place a message appears without a
   page load: a server-rendered error arrives with a whole new document, so
   nothing "moves" for the reader there. 20px is one 12px line at 1.4 plus the
   4px control gap — the height of both message shapes: the partial's
   `.m3e-text-field__error-text` and the `<p class="validation-error mt-1
   text-xs">` that form-validation.js inserts after the wrapper when the
   partial rendered no error node. `:not([hidden])` matters because the
   validator CLEARS a message by hiding its node, not by removing it. */
form[data-validate] .m3e-text-field:not(:has(> .m3e-text-field__error-text)):not(:has(> .validation-error:not([hidden]))) {
  padding-bottom: 20px;
}
/* The wrapper's own `gap` already supplies the 4px; do not pay for it twice. */
.m3e-text-field > .m3e-text-field__helper-text,
.m3e-text-field > .m3e-text-field__error-text,
.m3e-select-field > .m3e-text-field__helper-text,
.m3e-select-field > .m3e-text-field__error-text { margin-top: 0; }

/* Required marker */
.m3e-text-field__label .m3e-required { color: var(--danger); }
/* L1-04 / D5-21: the house convention marks the OPTIONAL fields, not the
   required ones — "(optional)" is a word, not a symbol, so it needs no legend,
   and it appears on the minority of fields rather than the majority. It is
   deliberately NOT aria-hidden: "optional" is information a screen-reader user
   wants, where the asterisk was decoration duplicating `aria-required`.
   Phase 2 makes it OPT-OUT (`mark_optional=False`) rather than automatic: it
   is meaningless on a filter, a page-size control or a saved view, and
   actively misleading on a ballot ("Your vote (optional)"). It never appears
   inside a radio or checkbox group's options — an individual option cannot be
   optional; you pick one. */
.m3e-optional {
  font-weight: 400;
  color: var(--fg2);
}
/* A6a (B4 request 2 / D3-15's other half). The mirror convention: a surface
   that marks its REQUIRED minority instead of its optional majority now has a
   house way to say so, in the same quiet weight and colour as "(optional)" —
   a word, not an asterisk, so it needs no legend. `mark_required=True` on the
   field partials emits it; it is never automatic, because whether a form marks
   the required or the optional half is a copy decision for the surface. */
.m3e-required-note {
  font-weight: 400;
  color: var(--fg2);
}

/* ── One state per state (D5 §S5) ──────────────────────────────────────────
 * FOCUS. The whole contract is: the global house 3px ring at 2px offset, plus
 * `border-color: var(--brand)` at UNCHANGED width. No shadows, no halos, no
 * width changes, no dark-only variants. Three components had invented their
 * own (four in dark) — a text field that grew 2px and reflowed the form, a
 * select drawing a second soft halo inside the ring, a Tom Select wrapper with
 * a dark-only `0 0 0 2px primary-container`. All deleted.
 *
 * ERROR. A 1px colour swap beside a 1px neutral border is not enough weight on
 * a form with eight errors scanned at arm's length. The inset shadow reads as
 * a 2px edge and costs no layout; the tint is what the eye actually catches.
 *
 * DISABLED. ONE idiom for every control. The text field used to have NONE —
 * it actively overrode the browser's own greying with
 * `color: on-surface !important`, so a disabled text field beside a disabled
 * dropdown showed one dead control and one live-looking one. */
.m3e-control:focus-visible,
.m3e-select:focus-visible,
.m3e-select-field select:focus-visible,
select:focus-visible,
textarea:focus-visible,
input[type="date"]:focus-visible,
input[type="time"]:focus-visible,
input[type="datetime-local"]:focus-visible,
input[type="month"]:focus-visible,
input[type="week"]:focus-visible,
input[type="search"]:focus-visible,
.ts-wrapper.focus.single {
  border-color: var(--brand);
}

input[aria-invalid="true"]:not([type="checkbox"]):not([type="radio"]),
select[aria-invalid="true"],
textarea[aria-invalid="true"],
.m3e-control[aria-invalid="true"],
.m3e-text-field--error .m3e-text-field__input-wrapper,
.m3e-text-field__input-wrapper:has([aria-invalid="true"]),
.m3e-select-field--error select,
.m3e-select-field--error .ts-wrapper.single,
.ts-wrapper.single:has(select[aria-invalid="true"]) {
  border-color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger);
  background-color: color-mix(in srgb, var(--danger-container) 35%, var(--surface-0));
}
.m3e-text-field--error .m3e-text-field__label,
.m3e-text-field:has([aria-invalid="true"]) .m3e-text-field__label,
.m3e-text-field:has([aria-invalid="true"]) .m3e-text-field__icon--trailing,
.m3e-select-field--error .m3e-text-field__label {
  color: var(--danger);
}
/* The control inside a text-field wrapper must not paint its own error box —
   the wrapper is the visible edge. */
.m3e-text-field__input-wrapper > [aria-invalid="true"] {
  box-shadow: none !important;
  background-color: transparent !important;
}

.m3e-text-field:has(:disabled),
.m3e-select-field:has(:disabled),
.m3e-choice:has(:disabled),
.m3e-checkbox:has(:disabled),
.m3e-radio:has(:disabled),
.m3e-switch:has(:disabled) { opacity: 0.38; }
.m3e-control:disabled,
select:disabled,
textarea:disabled,
input:disabled:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
.m3e-select:disabled,
.m3e-select-field select:disabled {
  background-color: var(--surface-2);
  color: var(--fg2);
  cursor: not-allowed;
}
:disabled { cursor: not-allowed; }
.m3e-text-field:has(:disabled) .m3e-text-field__input-wrapper {
  background-color: var(--surface-2);
  cursor: not-allowed;
}
/* Read-only is NOT disabled: the value is real and must stay full-strength.
   `.field-readonly` shipped for a year with zero call sites; this is the
   treatment it should always have had. */
[readonly],
.field-readonly {
  background-color: var(--surface-2);
  border-color: var(--border);
  cursor: default;
}
.m3e-text-field__input-wrapper:has(> [readonly]) {
  background-color: var(--surface-2);
  border-color: var(--border);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * TOOLBAR
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-toolbar {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-4);
  background-color: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-full);
}
.m3e-toolbar--floating {
  box-shadow: var(--md-sys-elevation-3);
}
.m3e-toolbar--docked {
  border-radius: 0;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
.m3e-toolbar__action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--md-sys-density-touch-target-default);
  height: var(--md-sys-density-touch-target-default);
  border-radius: var(--md-sys-shape-corner-full);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--md-sys-color-on-surface-variant);
  position: relative;
  overflow: hidden;
  transition: color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
  flex-shrink: 0;
  text-decoration: none;
}
.m3e-toolbar__action::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
  /* L7-11: without this the wash snapped on and off. */
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-toolbar__action:hover::before        { opacity: 0.08; } }
.m3e-toolbar__action:focus-visible::before { opacity: 0.12; }
.m3e-toolbar__action:active::before       { opacity: 0.16; }
.m3e-toolbar__action:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.m3e-toolbar__action--active { color: var(--md-sys-color-primary); }
.m3e-toolbar__action[aria-disabled="true"] { opacity: 0.38; cursor: not-allowed; pointer-events: none; }
.m3e-toolbar__action svg { width: 24px; height: 24px; }
.m3e-toolbar__divider {
  width: 1px;
  height: 24px;
  background-color: var(--md-sys-color-outline-variant);
  margin-inline: var(--md-sys-spacing-2);
  flex-shrink: 0;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * NAVIGATION BAR (bottom, mobile)
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-nav-bar {
  display: flex;
  /* L1-34: `stretch`, not `center` — every tab is the same height, so the one
     with a two-line label sets the bar's height and the others grow with it,
     instead of one tab standing 16px taller than its neighbours. */
  align-items: stretch;
  justify-content: space-around;
  background-color: var(--md-sys-color-surface-container);
  border-top: 1px solid var(--md-sys-color-outline-variant);
  padding-block: var(--md-sys-spacing-2);
  padding-bottom: max(var(--md-sys-spacing-2), env(safe-area-inset-bottom, 0px));
}
/* Ensure .hidden beats display:flex even when Tailwind utilities are in a lower-priority layer */
.m3e-nav-bar.hidden { display: none; }
.m3e-nav-bar__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--md-sys-spacing-1);
  /* L7-36 / L1-34: equal shares, so no label can widen its own tab, and
     `min-width: 0` so a long one shrinks rather than pushing the bar out of
     true. The auditor's "Finance reports" wrapped to two lines and stood 16px
     taller than "Home", "Sign-offs" and "More" — the owner's named Galaxy Z
     Fold complaint. The name is right (it matches the drawer row, the H1 and
     the page title, A8-20); the BAR was wrong. Every item now stretches to the
     bar's height with its icon pinned to the top and the same reserved label
     height, so a two-line label costs the bar one line and costs the row
     nothing. */
  flex: 1 1 0;
  min-width: 0;
  align-self: stretch;
  justify-content: flex-start;
  padding: var(--md-sys-spacing-1) var(--md-sys-spacing-1);
  text-decoration: none;
  color: var(--md-sys-color-on-surface-variant);
  position: relative;
  transition: color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
/* The indicator pill is the icon's row and never shrinks, so the icons on every
   tab share one baseline whatever the labels below them do. */
.m3e-nav-bar__indicator { flex: none; }
.m3e-nav-bar__item--active { color: var(--md-sys-color-on-secondary-container); }
.m3e-nav-bar__indicator {
  width: 64px;
  height: 32px;
  border-radius: var(--md-sys-shape-corner-full);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: transparent;
  /* Colour only: the pill is a fixed 64px, so there was never a width to
     animate (review P2-7). */
  transition: background-color var(--md-sys-motion-duration-medium2) var(--easing-effects);
  position: relative;
}
.m3e-nav-bar__item--active .m3e-nav-bar__indicator {
  background-color: var(--md-sys-color-secondary-container);
}
.m3e-nav-bar__icon {
  width: 24px;
  height: 24px;
}
.m3e-nav-bar__label {
  font-size: 12px;
  font-weight: 500;
  line-height: 16px;
  /* A stated behaviour rather than luck (L7-36, L1-34). Up to two lines, and
     the SAME reserved height on every tab in the bar — so a two-line label
     ("Finance reports") sits level with its one-line neighbours instead of
     pushing its own tab 16px taller than theirs. `text-wrap: balance` splits
     it evenly rather than leaving one word alone on the second line; the clamp
     is the last resort and, at 386px with five tabs, is not expected to fire.
     Bottom-nav labels are at most two short words. */
  width: 100%;
  min-height: 32px;                 /* two lines at 16px, reserved on every tab */
  text-align: center;
  text-wrap: balance;
  overflow: hidden;
  overflow-wrap: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-clamp: 2;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * NAVIGATION RAIL (sidebar, desktop)
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-nav-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--md-sys-spacing-1);
  width: 80px;
  padding: var(--md-sys-spacing-4) 0;
  background-color: var(--md-sys-color-surface-container-low);
  /* Dormant component (no template includes navigation_rail.html). Expanding
     is a layout change, so it snaps; it never animates width (P2-7). */
  overflow: hidden;
}
.m3e-nav-rail--expanded { width: 256px; align-items: flex-start; }
.m3e-nav-rail__fab {
  margin-bottom: var(--md-sys-spacing-4);
  align-self: center;
}
.m3e-nav-rail__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--md-sys-spacing-1);
  padding: var(--md-sys-spacing-1) 0;
  width: 100%;
  text-decoration: none;
  color: var(--md-sys-color-on-surface-variant);
  position: relative;
}
.m3e-nav-rail--expanded .m3e-nav-rail__item {
  flex-direction: row;
  gap: var(--md-sys-spacing-3);
  padding: var(--md-sys-spacing-1) var(--md-sys-spacing-4);
}
.m3e-nav-rail__item--active { color: var(--md-sys-color-on-secondary-container); }
.m3e-nav-rail__indicator {
  width: 56px;
  height: 32px;
  border-radius: var(--md-sys-shape-corner-full);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: transparent;
  transition: background-color var(--md-sys-motion-duration-medium2)
              var(--md-sys-motion-easing-expressive-spatial-default);
}
.m3e-nav-rail--expanded .m3e-nav-rail__indicator {
  width: 40px;
  height: 40px;
  border-radius: var(--md-sys-shape-corner-full);
}
.m3e-nav-rail__item--active .m3e-nav-rail__indicator {
  background-color: var(--md-sys-color-secondary-container);
}
.m3e-nav-rail__icon { width: 24px; height: 24px; flex-shrink: 0; }
.m3e-nav-rail__label {
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  max-width: 0;
  opacity: 0;
  transition: opacity var(--md-sys-motion-duration-short3) var(--easing-effects);
}
.m3e-nav-rail--expanded .m3e-nav-rail__label { max-width: 160px; opacity: 1; }
.m3e-nav-rail__collapse-btn {
  margin-top: auto;
  width: 40px;
  height: 40px;
  border-radius: var(--md-sys-shape-corner-full);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--md-sys-color-on-surface-variant);
  display: flex;
  align-items: center;
  justify-content: center;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * CLUB LOGO TILE (m3e/shell/top_bar.html, auth_gate_bar.html)
 *
 * The 36px logo tile. It was painted with `background: var(--gradient-mark)`
 * and an inline `color`, i.e. a background-IMAGE with no resolvable
 * background-colour underneath - so every contrast tool (including the house
 * a11y harness, A8-41) computed the tile against `rgba(0,0,0,0)` and reported
 * ~1.05:1 in the high-contrast modes, where the real measured pair is 16:1
 * light and 19:1 dark. State the flat colour under the gradient and the ink
 * from the same token family, so the pair is legible to a reader AND provable
 * to a tool.
 * ═══════════════════════════════════════════════════════════════════════════ */

.npsc-mark {
  display: flex;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
}

/* The club logo (scripts/npsc_logo.mjs): a squash court from above with NP
   in the front court. It paints its own night tile, so it looks the same in
   every theme and needs no ink token. */
.npsc-logo {
  display: block;
  width: 100%;
  height: 100%;
  /* The tile is the night colour, which is also the dark theme's page. A
     hairline in the ink colour keeps its edge visible there; on light pages
     it is barely there. 18.75% = the tile's rx 12 on its 64-unit grid. */
  border-radius: 18.75%;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) 28%, transparent);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * IMPERSONATION BANNER (base.html)
 *
 * Q8-07: a plain static <div> with no role and no live region. A screen-reader
 * user was never told the session identity had changed, and one scroll removed
 * the only reminder that everything on screen belongs to someone else. The
 * template now carries `role="status"`; this makes it stay put.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-impersonation-banner {
  /* .m3e-sticky-below-top-bar supplies position + top; this only has to sit
     above page content and give itself a background, because a sticky element
     over scrolling rows must be opaque. */
  z-index: var(--z-banner);
  margin: var(--md-sys-spacing-4) var(--md-sys-spacing-4) 0;
  padding-bottom: var(--md-sys-spacing-2);
  background: var(--md-sys-color-surface);
  border-radius: var(--md-sys-shape-corner-medium);
}
@media (min-width: 768px) {
  .m3e-impersonation-banner { padding-inline: var(--md-sys-spacing-2); }
}
.m3e-impersonation-banner__stop {
  background: var(--danger);
  color: var(--danger-on);
  border: none;
}
@media (hover: hover) and (pointer: fine) {
  .m3e-impersonation-banner__stop:hover {
    background: color-mix(in srgb, var(--danger) 88%, black);
    color: var(--danger-on);
  }
}

/* Standalone text actions (Q1-23). A sentence-internal link stays a link; a
   link that IS an action on its own line needs a target. */
.npsc-text-action {
  display: inline-flex;
  align-items: center;
  color: var(--md-sys-color-primary);
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (max-width: 839px) {
  .npsc-text-action { min-height: 44px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * NAVIGATION DRAWER
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-nav-drawer {
  display: flex;
  flex-direction: column;
  width: 360px;
  max-width: 90vw;
  height: 100%;
  background-color: var(--md-sys-color-surface-container-low);
  border-radius: 0 var(--md-sys-shape-corner-large) var(--md-sys-shape-corner-large) 0;
  overflow-y: auto;
  overflow-x: hidden;
}
.m3e-nav-drawer--permanent {
  border-radius: 0;
  /* Q4-02: the drawer itself no longer scrolls. Its BODY does, and the footer
     is a sibling that can never cover a destination. */
  overflow: hidden;
}

/* The scrolling half of the permanent drawer (Q4-02, Q5-14).
   `min-height: 0` is what actually lets a flex child scroll instead of growing
   the column; without it `overflow-y: auto` is inert. */
/* Q8-32: the quick-find input was 36px - the desktop compact token, so within
   the rules, but the smallest control in the drawer and the one a mouse aims at
   most often. */
.m3e-nav-drawer__quick-find {
  /* L4-10: 40px is under the touch floor, and the drawer is a touch surface on
     a Fold in landscape as much as on a laptop. */
  min-height: 44px;
  background: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-on-surface);
}

.m3e-nav-drawer__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* A little breathing room so the last row does not sit flush on the footer
     rule, and a scroll-padding so scrollIntoView never parks a row half under
     the search field. */
  padding-bottom: var(--md-sys-spacing-2);
  scroll-padding-block: 56px var(--md-sys-spacing-4);
  scrollbar-width: thin;
}

/* Scroll affordance. `.m3e-nav-drawer__body` gets `--scrollable` from
   m3e-shell.js when its content overflows, and `--scrolled-end` when the reader
   has reached the bottom; the fade is the same treatment
   `.m3e-table-wrapper--scrollable` uses, so "there is more this way" reads the
   same everywhere in the product. */
.m3e-nav-drawer__body--scrollable {
  position: relative;
  /* D3-58: the list WAS scrollable and nothing said so — no scrollbar, no
     fade, no indicator; the only clue was that a destination was sliced in
     half by the drawer's edge. The fade below is one half of the answer; a
     reserved, always-visible scrollbar gutter is the other, so a clipped
     drawer reads as deliberate rather than broken. */
  scrollbar-gutter: stable;
  /* Two custom properties, one mask: the fade appears only at the end the
     reader can still travel towards, so "there is more this way" is never a
     lie. m3e-shell.js sets them from the scroll position. */
  --drawer-fade-top: 0px;
  --drawer-fade-bottom: 28px;
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--drawer-fade-top),
    #000 calc(100% - var(--drawer-fade-bottom)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--drawer-fade-top),
    #000 calc(100% - var(--drawer-fade-bottom)),
    transparent 100%
  );
}
.m3e-nav-drawer__body--scrolled-start { --drawer-fade-top: 28px; }
.m3e-nav-drawer__body--scrolled-end { --drawer-fade-bottom: 0px; }

/* A2 · Decision 9 / D6-25: ONE drawer width.
 *
 * The auditor surface used to run a 208px drawer with 48px rows while
 * committee ran 240px with 44px, so a person with both accesses watched the
 * content column jump 32px sideways when they switched portal. Nothing in the
 * design system justified two rail widths, and the auditor's six labels fit
 * 240px comfortably. The token is declared once, in the shell, and no surface
 * overrides it. */

/* The permanent drawer has no footer. It carried an identity block that
   opened the account menu — the same control the top bar already carries, so
   a committee laptop showed it twice at once (top right and bottom left).
   One trigger per viewport; the block is gone and the rows have the space.
   (Before that it was `position: sticky; bottom: 0` inside the drawer's own
   scroll container, painting over the last two destinations at 1440x900 —
   Q4-02.) */
.m3e-nav-drawer__headline {
  font-size: 14px;
  color: var(--md-sys-color-on-surface-variant);
  padding: var(--md-sys-spacing-4) var(--md-sys-spacing-6) var(--md-sys-spacing-2);
}
.m3e-nav-drawer__item {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  /* 56px is the M3 spec height for a modal drawer with a handful of rows. The
     committee permanent drawer carries seventeen; at 56px they need 1343px of
     an 836px column (Q4-02, Q5-14). 44px is the touch floor and the honest
     density for a permanent desktop drawer. The mobile drawer keeps 56px
     below. */
  height: 44px;
}
/* A2 · Decision 9 / D6-18: the PERMANENT drawer goes to 40px.
   1,058px of destinations in a 692px column meant six of seventeen were never
   visible on a 1366x768 laptop — the most common laptop in the world could not
   display this app's primary navigation. 40px is a pointer density on a
   pointer surface (the permanent drawer only exists at >=840px); the modal
   drawer, which is the touch one, keeps 56px. */
.m3e-nav-drawer--permanent .m3e-nav-drawer__item { height: 40px; }
@media (max-width: 839px) {
  .m3e-nav-drawer:not(.m3e-nav-drawer--permanent) .m3e-nav-drawer__item { height: 56px; }
}
.m3e-nav-drawer__item {
  padding: 0 var(--md-sys-spacing-4);
  border-radius: var(--md-sys-shape-corner-full);
  /* L7-09: 2px left two neighbouring pill washes all but touching. */
  margin: 3px var(--md-sys-spacing-3);
  flex-shrink: 0;
  text-decoration: none;
  color: var(--md-sys-color-on-surface);
  position: relative;
  /* A8-58: `overflow:hidden` here existed to clip the ::before ripple to the
     pill radius, but it also clipped the trailing notification badge to a
     sliver — the count, which is the whole point of the badge, was unreadable.
     The clip now lives on the ripple layer itself. */
  transition: background-color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
/* Level-2 (group child) rows: IA spec S3 asks for a 16px indent, not the 40px
   `pl-10` that was truncating labels in a 240px drawer ("Court requireme...").
   Every nav destination is level-2 (section headers already convey the
   grouping), so children carry only an indent — no rail. The active row's
   primary indicator (`.m3e-nav-drawer__item--active::after` below) is the only
   left-edge mark that ever renders. */
.m3e-nav-drawer__item--child {
  font-size: 14px;
  padding-left: calc(var(--md-sys-spacing-4) + var(--md-sys-spacing-3));
}

.m3e-nav-drawer__item::before {
  content: '';
  position: absolute;
  /* L7-09: the rows are block-level, so adjacent vertical margins COLLAPSE -
     `margin: 2px` gave 2px between two rows, not 4px, and the measured gap
     between two neighbouring pill washes was 2px. 3px of margin (collapsed)
     plus 3px of wash inset each side is 9px of unpainted ground, without
     adding sixteen rows' worth of height to a drawer that already carries
     seventeen destinations in one column. */
  inset: 3px 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-nav-drawer__item:hover::before        { opacity: 0.08; } }
.m3e-nav-drawer__item:active::before       { opacity: 0.16; }
.m3e-nav-drawer__item:focus-visible::before { opacity: 0.12; }
.m3e-nav-drawer__item:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: -3px;
}
/* Two-class specificity (0,2,0) so the active pairing wins over the legacy
 * `.sidebar-link` compat class (single-class, later in source order), which
 * otherwise forces `color: on-surface` onto the active item. That override was
 * invisible in standard modes (light secondary-container) but failed WCAG
 * 1.4.11 / AA in high-contrast modes, where secondary-container flips dark and
 * the on-surface text/icon lost contrast against it. */
.m3e-nav-drawer__item.m3e-nav-drawer__item--active {
  position: relative;
  background-color: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
.m3e-nav-drawer__item--active::after {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 60%;
  border-radius: 0 var(--md-sys-shape-corner-full) var(--md-sys-shape-corner-full) 0;
  background: var(--md-sys-color-primary);
}
.m3e-nav-drawer__item-icon { width: 24px; height: 24px; flex-shrink: 0; }
.m3e-nav-drawer__item-label {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-nav-drawer__section-header {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
  padding: var(--md-sys-spacing-4) var(--md-sys-spacing-6) var(--md-sys-spacing-2);
}
.m3e-nav-drawer__section-header:not(:first-child) {
  margin-top: var(--md-sys-spacing-2);
  padding-top: var(--md-sys-spacing-4);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}
/* Five group labels at 40px each is 200px of a 836px column that carries no
   destination. In the permanent drawer they are quieter and shorter (Q4-02).

   A2 / D6-18: pinned to a flat 28px block, the same whether or not the label
   carries the group rule, so the drawer's height is arithmetic anyone can
   check: rows x 40 + labels x 28 + gaps. */
.m3e-nav-drawer--permanent .m3e-nav-drawer__section-header {
  padding: 10px var(--md-sys-spacing-6) 2px;
  font-size: 11px;
  line-height: 16px;
  flex-shrink: 0;
}
.m3e-nav-drawer--permanent .m3e-nav-drawer__section-header:not(:first-child) {
  margin-top: 0;
  padding-top: 10px;
}
/* D6-18 move 3: sticky group labels, so a scrolled drawer still says which
   group you are in. The permanent drawer's body is the scroll container; the
   label needs its own background or the rows would read through it. */
.m3e-nav-drawer--permanent .m3e-nav-drawer__section-header {
  position: sticky;
  top: 0;
  z-index: 1;
  background-color: var(--md-sys-color-surface-container-low);
}
/* The rows are 40px and the wash is inset 3px, so 2px of margin leaves 8px of
   unpainted ground between two neighbouring pills — the same optical gap the
   3px margin gave at 44px, for 14px less per row. */
.m3e-nav-drawer--permanent .m3e-nav-drawer__item {
  margin-block: 2px;
}
/* A group label with no rows under it is a bug the reader should never see
   (Q4-02, Q5-14). Quick-find hides rows; when every row in a group is hidden
   the label goes with them. m3e-shell.js sets `hidden` on the label. */
.m3e-nav-drawer__section-header[hidden] { display: none; }
/* ── Modal drawer: scrim + slide-in ── */
.m3e-nav-drawer-scrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  align-items: stretch;
}
.m3e-nav-drawer-scrim[data-m3e-open] { display: flex; }
.m3e-nav-drawer--modal {
  border-radius: 0 var(--md-sys-shape-corner-large) var(--md-sys-shape-corner-large) 0;
  /* Edge-anchored: the standard spatial curve (overshoot under 1px) keeps the
     drawer on its edge; the expressive one pulled a strip of page into view
     at the left edge mid-entrance. */
  animation: m3e-drawer-enter var(--duration-med)
             var(--md-sys-motion-easing-standard-spatial-default);
  pointer-events: auto;
}
@keyframes m3e-drawer-enter {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}
/* Exit (closeOverlay → `m3e-overlay--closing`): faster than the entrance,
   `--ease-drop`, painted but unclickable until the animation ends. */
.m3e-nav-drawer-scrim.m3e-overlay--closing {
  display: flex;
  pointer-events: none;
  animation: menu-backdrop-out var(--duration-quick) var(--ease-drop) forwards;
}
/* The panel re-enables pointer-events; a closing panel must not take taps. */
.m3e-overlay--closing .m3e-nav-drawer--modal { pointer-events: none; }
.m3e-overlay--closing .m3e-nav-drawer--modal {
  animation: menu-panel-out var(--duration-quick) var(--ease-drop) forwards;
}
.m3e-nav-drawer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-3) 0;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * TOP APP BAR
 * ═══════════════════════════════════════════════════════════════════════════ */

:root { --shell-top-bar-height: 64px; }
.m3e-top-app-bar {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-1);
  min-height: var(--shell-top-bar-height);
  padding: 0 var(--md-sys-spacing-1);
  background-color: var(--md-sys-color-surface);
  position: relative;
  isolation: isolate;
  transition:
    background-color var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-standard-effects-default);
}
/* The scrolled elevation fades by opacity on a layer that carries the shadow,
   instead of interpolating box-shadow (a repaint every frame; review P2-6). */
.m3e-top-app-bar::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: var(--md-sys-elevation-2);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-top-app-bar--scrolled::after { opacity: 1; }
.m3e-top-app-bar--fixed {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-chrome);
}
.m3e-top-app-bar--scrolled {
  background-color: var(--md-sys-color-surface-container);
}
.m3e-top-app-bar__icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--md-sys-density-touch-target-default);
  height: var(--md-sys-density-touch-target-default);
  border-radius: var(--md-sys-shape-corner-full);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--md-sys-color-on-surface);
  position: relative;
  overflow: hidden;
  flex-shrink: 0;
  text-decoration: none;
}
.m3e-top-app-bar__icon-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
  /* L7-11: without this the wash snapped on and off. */
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-top-app-bar__icon-btn:hover::before        { opacity: 0.08; } }
.m3e-top-app-bar__icon-btn:focus-visible::before { opacity: 0.12; }
.m3e-top-app-bar__icon-btn:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.m3e-top-app-bar__icon-btn svg { width: 24px; height: 24px; }
.m3e-top-app-bar__title {
  flex: 1;
  font-size: 22px;
  font-weight: 400;
  color: var(--md-sys-color-on-surface);
  padding: 0 var(--md-sys-spacing-3);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.m3e-top-app-bar__actions {
  display: flex;
  align-items: center;
}
/* Embedded search bar in top app bar */
.m3e-top-app-bar__search {
  flex: 1;
  box-shadow: none;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * SEARCH BAR
 * ═══════════════════════════════════════════════════════════════════════════ */

/* D5-18: ONE search control. The app shipped three treatments in one viewport
   — a 48px bordered page search, a 44px borderless `#EDEDF4` column filter,
   and this, a 56px elevated pill. A search field is a field: it takes the
   house box, a leading 20px magnifier and a trailing clear control. The
   borderless fill survives in the drawer only, as an explicit modifier. */
.m3e-search-bar {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  min-height: var(--control-h-form);
  padding-inline: var(--control-pad-x);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--control-fill);
  color: var(--control-ink);
  transition: border-color var(--duration-quick) var(--ease-rally);
}
.m3e-search-bar:focus-within { border-color: var(--brand); }
.m3e-search-bar--fill {
  border-color: transparent;
  background-color: var(--surface-2);
}
.m3e-search-bar__icon {
  width: var(--control-glyph);
  height: var(--control-glyph);
  flex-shrink: 0;
  color: var(--fg2);
}
.m3e-search-bar__icon svg { width: 100%; height: 100%; }
.m3e-search-bar__input {
  flex: 1;
  min-width: 0;
  min-height: 0;
  border: none;
  padding: 0;
  background: transparent;
  outline: none;
  font-size: var(--control-fs);
  color: var(--control-ink);
}
.m3e-search-bar__input::-webkit-search-cancel-button { appearance: none; }
.m3e-search-bar__input::placeholder { color: var(--md-sys-color-on-surface-variant); }
/* Q8-18: the bare <input> inside the bar computed 24px tall. The bar around it
   is taller, so a tap near the text usually landed — "usually" is not a touch
   target. This is the same 44px floor as every other control below 840px
   (PLATFORM_R4 §11), not a fourth `!important` exception. */
@media (max-width: 839px) {
  .m3e-search-bar__input { min-height: 44px !important; }
}
.m3e-search-bar__trailing { width: 24px; height: 24px; flex-shrink: 0; color: var(--md-sys-color-on-surface-variant); }


/* ═══════════════════════════════════════════════════════════════════════════
 * TABS
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-tabs {
  position: relative;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
/* ── A tab strip WRAPS. It never scrolls, at any width. ──────────────────
 *
 * This was a scrolling rail with edge fades, and by its own terms it worked:
 * nothing was chopped, every label was reachable, the fade said there was
 * more. It still showed "Patte…", "Reimburs…" and "Op…" against the card edge,
 * and a word cut off is a word cut off — a gesture that WOULD reveal it is not
 * an answer to "I can't read that". The objection is to the cut word, not to
 * the width, so the answer cannot be a breakpoint either: every tab is on
 * screen, on at most two rows, on a 386px phone and on a 1440px laptop alike.
 *
 * A second row costs one line of vertical rhythm. Reading all four
 * destinations is worth it, and it is what the owner asked for.
 *
 * `gap: 4px 8px` — 8px between two tabs in a row (the ≥8px rule is about the
 * CONTROLS, not only their washes: 4px of gap plus a 2px wash inset kept the
 * paint apart but still put two 44px hit areas 4px from each other, L6-15 /
 * L2-36), and 4px between rows, which with the 4px/6px wash inset reads as
 * air without the rows drifting apart.
 * ─────────────────────────────────────────────────────────────────────── */
.m3e-tabs__list {
  display: flex;
  flex-wrap: wrap;
  /* A6a (A6b request 2): the ROW gap is 8px too, not 4px. The note above
     applies the 8px control-separation rule to the column gap and then makes
     an exception for rows on the grounds that the wash inset "reads as air" —
     but the rule is about hit areas, not paint, and a strip that wraps puts
     two 44px targets 4px apart in exactly the direction a thumb travels least
     accurately. Seven account tabs at 386px is a two-row strip on the member
     surface's primary device. A6b worked around it with a local `gap-y-2`;
     that utility can come off now. */
  gap: var(--md-sys-spacing-2);   /* 8px, both axes */
  overflow: visible;
  position: relative;
  /* A flex item's default min-width is `auto`, so a strip wider than its
     container used to propagate its min-content width up through every
     ancestor and scroll the whole PAGE sideways. Wrapping removes the cause,
     but the guard is free and a nested flex ancestor can still reintroduce it. */
  min-width: 0;
}

.m3e-tabs__tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--md-sys-spacing-1);
  /* Natural width, never squeezed. `min-width: 90px` + unshrinkable padding is
     what chopped "Reimbursement" to "eimburse" (L7-01): flex-shrink pulled the
     tab to its 90px floor and `overflow: hidden` ate the label. The strip
     wraps instead, so a tab that does not fit the row starts the next one. */
  flex: 0 0 auto;
  min-width: 0;
  height: 48px;
  padding: 0 var(--md-sys-spacing-4);
  background: none;
  border: none;
  cursor: pointer;
  color: var(--md-sys-color-on-surface-variant);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.1px;
  white-space: nowrap;
  position: relative;
  overflow: hidden;
  transition: color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-tabs__tab::before {
  content: '';
  position: absolute;
  /* Inside the box, clear of the 3px sliding indicator, rounded (L7-06). */
  inset: 4px 2px 6px;
  border-radius: var(--radius-sm);
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-tabs__tab:hover::before        { opacity: 0.08; } }
.m3e-tabs__tab:focus-visible::before { opacity: 0.12; }
.m3e-tabs__tab:active::before        { opacity: 0.16; }
.m3e-tabs__tab:focus-visible { outline: none; box-shadow: inset 0 0 0 3px var(--md-sys-color-primary); }
/* A3-26 (retired by L7-17): the outset ring on every `[role="tab"]` was clipped
   to nothing by the meeting strips' `overflow-hidden` card. Tab-shaped controls
   now take the sanctioned INSET ring from the house focus block at the end of
   this file. `.gm-tab-active` / `.gm-tab-inactive` appeared in no template and
   are gone. */
/* A3-27: 42px tabs on the surface whose primary device is a phone. */
@media (max-width: 839px) {
  .m3e-tabs__tab,
  [role="tab"] {
    min-height: 44px;
  }
}
/* Below 480px, tighter metrics so four tabs need one row rather than two.
   The wrapping itself is unconditional; only the padding and the type step
   down here. */
@media (max-width: 479px) {
  .m3e-tabs__tab {
    padding-inline: var(--md-sys-spacing-2);
    font-size: 13px;
    height: auto;
    min-height: 44px;
  }
}

/* ── A wrapped strip draws its own underline ─────────────────────────────
 *
 * The sliding indicator is one absolutely positioned bar; it cannot follow a
 * strip that has two rows. CSS cannot ask "did this wrap?", so `initTabs()` and
 * `initTabRails()` measure it (`tabsAreWrapped()` — any tab whose `offsetTop`
 * differs from the first tab's) and put `--wrapped` on the list, on init, on
 * every activation and on resize in BOTH directions. A strip that wraps at
 * 860px and fits at 1440px therefore gets the per-tab underline at one width
 * and the sliding bar at the other, without a page reload.
 *
 * Same 3px, same colour, same top radius as the slider it stands in for. */
.m3e-tabs__list--wrapped .m3e-tabs__indicator { display: none; }
.m3e-tabs__list--wrapped .m3e-tabs__tab[aria-selected="true"]::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
  border-radius: 3px 3px 0 0;
  /* Same neutral-ink budget as the slider above. */
  background-color: var(--md-sys-color-on-surface);
}
.m3e-tabs--secondary .m3e-tabs__list--wrapped .m3e-tabs__tab[aria-selected="true"]::after {
  background-color: var(--md-sys-color-on-surface);
}
.m3e-tabs__tab[aria-selected="true"] { color: var(--md-sys-color-primary); }
.m3e-tabs__tab-icon { width: 24px; height: 24px; flex-shrink: 0; }
.m3e-tabs__tab-label-row {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-1);
  /* So the badge keeps its 16px and the label is what gives way (L7-29). */
  min-width: 0;
  max-width: 100%;
}
.m3e-tabs__tab-label {
  line-height: 1;
  /* Last resort only. With `flex: 0 0 auto` on the tab this should never fire —
     the strip scrolls first — but a bare chop is never acceptable (L7-01). */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.m3e-tabs__indicator {
  position: absolute;
  bottom: 0;
  height: 3px;
  /* Court & Print budget: the sliding marker is WHERE YOU ARE, not an
   * action. It moves to neutral ink so the court red stays on the page's
   * one primary action (redesign-2026 rule 2). */
  background-color: var(--md-sys-color-on-surface);
  border-radius: 3px 3px 0 0;
  /* Seg-pill pattern (review P1-5): m3e-components.js writes `width` and
     `top` instantly and moves the bar with transform: translateX(), so only
     the compositor-friendly transform animates. --ease-rally is the motion
     spec's tab-switch curve. The first placement is a jump (`transition:
     none` until `data-tabs-placed`), not a slide in from the left edge. */
  left: 0;
  transition: transform var(--md-sys-motion-duration-medium1) var(--ease-rally);
}
/* Secondary tabs */
.m3e-tabs--secondary .m3e-tabs__tab[aria-selected="true"] {
  color: var(--md-sys-color-on-surface);
}
.m3e-tabs--secondary .m3e-tabs__indicator {
  background-color: var(--md-sys-color-on-surface);
}
/* Tab panels */
.m3e-tabs__panel[hidden] { display: none; }
.m3e-tabs__panel { padding-block: var(--md-sys-spacing-4); }
/* Tab switch: the newly shown panel fades up (set by activateTab in
   m3e-components.js; removed on animationend). */
.m3e-tabs__panel--entering {
  animation: vt-fade-in var(--md-sys-motion-duration-short4)
             var(--md-sys-motion-easing-standard-effects-default);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * MENU
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-menu {
  display: none;
  flex-direction: column;
  background-color: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-extra-small);
  box-shadow: var(--md-sys-elevation-2);
  padding: var(--md-sys-spacing-2) 0;
  min-width: 112px;
  max-width: 280px;
  position: absolute;
  z-index: var(--z-menu);        /* L7-34: above every scrim */
  outline: none;
  /* Enter animation */
  transform-origin: top left;
  animation: m3e-menu-enter var(--md-sys-motion-duration-medium1)
             var(--md-sys-motion-easing-expressive-spatial-default);
}
.m3e-menu[data-m3e-open] { display: flex; }
@keyframes m3e-menu-enter {
  from { opacity: 0; transform: scale(0.9); }
  to   { opacity: 1; transform: scale(1); }
}
/* Exit: closeOverlay() drops `data-m3e-open` at once (so every keyboard and
   click-outside handler already treats the menu as closed) and keeps it
   painted with `--closing` for the length of this fade. Not clickable while
   it fades. */
.m3e-menu.m3e-menu--closing {
  display: flex;
  pointer-events: none;
  animation: m3e-menu-exit var(--md-sys-motion-duration-short3)
             var(--md-sys-motion-easing-standard-effects-default) forwards;
}
@keyframes m3e-menu-exit {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(0.95); }
}
/* L1-29: the account-menu row used to paint a full-bleed square rectangle the
   whole width of the menu, touching the dividers above and below - while the
   drawer row, doing the same job, is an inset 8px pill. Same treatment here. */
.m3e-menu__item {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  height: var(--md-sys-density-touch-target-default);
  padding: 0 var(--md-sys-spacing-3);
  margin: 2px var(--md-sys-spacing-2);
  width: calc(100% - (var(--md-sys-spacing-2) * 2));
  border-radius: var(--md-sys-shape-corner-small);
  cursor: pointer;
  color: var(--md-sys-color-on-surface);
  font-size: 14px;
  background: none;
  border: none;
  text-align: start;
  text-decoration: none;
  width: 100%;
  position: relative;
  overflow: hidden;
  white-space: nowrap;
}
.m3e-menu__item::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;                 /* L7-12 */
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  /* L7-11: without this the wash snapped on and off. */
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-menu__item:hover::before        { opacity: 0.08; } }
.m3e-menu__item:focus-visible::before { opacity: 0.12; }
.m3e-menu__item:active::before       { opacity: 0.16; }
.m3e-menu__item:focus-visible { outline: none; box-shadow: inset 0 0 0 3px var(--md-sys-color-primary); }
.m3e-menu__item--destructive { color: var(--md-sys-color-error); }
.m3e-menu__item[aria-disabled="true"] { opacity: 0.38; cursor: not-allowed; pointer-events: none; }
.m3e-menu__item-icon { width: 24px; height: 24px; flex-shrink: 0; color: var(--md-sys-color-on-surface-variant); }
.m3e-menu__item--destructive .m3e-menu__item-icon { color: var(--md-sys-color-error); }
/* A6a (B2 request 3 / D2-69): committee and auditor are pointer-precision
   surfaces and set their own control height everywhere else (see
   `--control-h-form` in colors_and_type.css); the menu was the one component
   that stayed at the 48px touch default there, so a six-item row menu on the
   court plan was ~310px of popup over a 56px row. 44px is still the touch
   floor — it is the density step, not a shortcut past it. The member and
   public surfaces keep 48px. */
[data-portal-surface="committee"] .m3e-menu__item,
[data-portal-surface="auditor"] .m3e-menu__item {
  height: 44px;
  font-size: 14px;
}
.m3e-menu__divider {
  border: none;
  border-top: 1px solid var(--md-sys-color-outline-variant);
  margin: var(--md-sys-spacing-2) 0;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * ACCOUNT IDENTITY — one trigger, one menu, every portal
 *
 * The top bar, the permanent drawer footer and the phone sheet all show the
 * same block: an initials avatar, the name, and (in the drawer/sheet) the role
 * line. They open the one account menu, so a person who is member, committee
 * and auditor at once sees the same surface however they arrive at it.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-avatar {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  user-select: none;
}

/* Top-bar trigger. 44px tall so it meets the touch floor at every width; the
   first name appears from `md` up, where the bar has the room for it. */
.m3e-account-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  min-height: 44px;
  padding: 0 var(--md-sys-spacing-2);
  border: none;
  background: none;
  cursor: pointer;
  border-radius: var(--md-sys-shape-corner-full);
  color: var(--md-sys-color-on-surface);
}
/* L7-11: state layer instead of an opaque surface swap, and a transition it
   never had. L7-28 / L1-26 / L2-04: `min-width: 0` so the trigger shrinks
   inside the flex line instead of being pushed off the right edge. */
.m3e-account-trigger { position: relative; isolation: isolate; min-width: 0; }
.m3e-account-trigger::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-account-trigger:hover::before        { opacity: 0.08; } }
.m3e-account-trigger:focus-visible::before { opacity: 0.12; }
.m3e-account-trigger:active::before       { opacity: 0.16; }
.m3e-account-trigger__name {
  display: none;
  max-width: 10ch;
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 500;
}
@media (min-width: 768px) {
  .m3e-account-trigger__name { display: inline; }
}
/* L7-28 / L1-26 / L2-04: between 840 and 1023px the desktop drawer AND the full
   top bar both render, and on the member surface six nav links join them -
   884px of content in an 842px box, so the account trigger was sliced by the
   viewport edge. The avatar alone is the M3 idiom at this width. */
@media (min-width: 840px) and (max-width: 1023px) {
  .m3e-account-trigger__name { display: none; }
}

/* Drawer-footer / phone-sheet identity block. */
.m3e-account-block {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  width: 100%;
  min-height: 56px;
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-4);
  border: none;
  background: none;
  cursor: pointer;
  text-align: start;
  color: var(--md-sys-color-on-surface);
  list-style: none;
}
.m3e-account-block::-webkit-details-marker { display: none; }
/* L7-11: state layer instead of an opaque surface swap, and a transition. */
.m3e-account-block { position: relative; isolation: isolate; }
.m3e-account-block::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-account-block:hover::before        { opacity: 0.08; } }
.m3e-account-block:focus-visible::before { opacity: 0.12; }
.m3e-account-block:active::before       { opacity: 0.16; }
.m3e-account-block__text { min-width: 0; display: flex; flex-direction: column; }
.m3e-account-block__name {
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-account-block__email,
.m3e-account-block__role {
  font-size: 12px;
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The summary form carries a chevron, like every other disclosure here. */
.m3e-account-block--summary::after {
  content: '';
  margin-left: auto;
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-account-disclosure[open] .m3e-account-block--summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

/* The popup. Wider than a plain .m3e-menu: it carries a segmented control and,
   for an admin, the impersonation picker. */
.m3e-account-menu {
  width: 20rem;
  max-width: calc(100vw - 16px);
  max-height: calc(100vh - 88px);
  max-height: calc(100dvh - 88px);
  overflow-y: auto;
  border-radius: var(--md-sys-shape-corner-large);
  box-shadow: var(--md-sys-elevation-3);
}
/* Inline twin, for the phone sheet where a popup cannot open. */
.m3e-account-panel { display: flex; flex-direction: column; }

.m3e-account-menu__identity { padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4); }
.m3e-account-menu__section { padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4); }
.m3e-account-menu__overline {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--md-sys-color-on-surface-variant);
}
.m3e-account-menu__name {
  margin-top: 2px;
  font-size: 14px;
  font-weight: 600;
  color: var(--md-sys-color-on-surface);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-account-menu__email {
  font-size: 13px;
  color: var(--md-sys-color-on-surface-variant);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-account-menu__appearance-summary { list-style: none; }
/* Kenny 2026-09-26: in the account panel some rows (the Guide <button>, the
   Appearance <summary>) took the full width AND the item's 8px side margins,
   so the panel ran 8px wide and scrolled sideways, pushing the Appearance
   chevron to the edge. The panel is a flex column: auto width fills exactly
   the space between the margins for every row, whatever the element. */
.m3e-account-panel .m3e-menu__item,
.m3e-account-menu .m3e-menu__item { width: auto; }
/* .m3e-menu is already a flex column when open ([data-m3e-open]). */
.m3e-account-panel form:has(> .m3e-menu__item),
.m3e-account-menu form:has(> .m3e-menu__item),
.m3e-account-menu details:has(> .m3e-menu__item) ,
.m3e-account-panel details:has(> .m3e-menu__item) { display: flex; flex-direction: column; }
.m3e-account-menu__appearance-summary::-webkit-details-marker { display: none; }

/* The Court plan "This month" rail summary draws its own chevron, so the
   native marker is suppressed the same way: `list-style` for every modern
   engine, plus the WebKit pseudo-element Safari still honours. Tailwind's
   `list-none` on the element covers the first half only. */
.m3e-court-plan-rail__summary { list-style: none; }
.m3e-court-plan-rail__summary::-webkit-details-marker { display: none; }
.m3e-account-menu__appearance-summary::after {
  content: '';
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-account-menu__appearance[open] .m3e-account-menu__appearance-summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * TOAST PILL (legacy glass-pill replacement)
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-toast-pill {
  border-radius: var(--md-sys-shape-corner-full);
  background-color: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  box-shadow: var(--md-sys-elevation-3);
}
.m3e-toast-pill svg { color: inherit; }


/* ═══════════════════════════════════════════════════════════════════════════
 * SNACKBAR
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-snackbar-container {
  position: fixed;
  z-index: var(--z-snackbar);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  pointer-events: none;
}
.m3e-snackbar-container--bottom-center {
  /* One row of the sticky .m3e-form-actions bar: its block padding, one
     --control-h-form button and its 1px top border (see that block). */
  --m3e-snackbar-form-actions-lift: calc(
    var(--control-h-form) + (var(--md-sys-spacing-3) * 2) + 1px + var(--md-sys-spacing-2)
  );
  bottom: max(var(--md-sys-spacing-6), env(safe-area-inset-bottom, 16px));
  left: 50%;
  transform: translateX(-50%);
}
/* P1-10: a page with a sticky form action bar lifts the snackbar clear of it,
   so an error toast never sits on the Save button the reader needs. */
body:has(.m3e-form-actions) .m3e-snackbar-container--bottom-center {
  bottom: calc(max(var(--md-sys-spacing-6), env(safe-area-inset-bottom, 16px)) + var(--m3e-snackbar-form-actions-lift));
}
/* Raise above mobile bottom nav bar (hidden at 840px). Same token the form
   action bar uses, so the two cannot drift apart. */
@media (max-width: 839px) {
  .m3e-snackbar-container--bottom-center {
    bottom: calc(var(--m3e-bottom-nav-height) + env(safe-area-inset-bottom, 0px));
  }
  body:has(.m3e-form-actions) .m3e-snackbar-container--bottom-center {
    bottom: calc(var(--m3e-bottom-nav-height) + env(safe-area-inset-bottom, 0px) + var(--m3e-snackbar-form-actions-lift));
  }
}
.m3e-snackbar-container--bottom-start { bottom: var(--md-sys-spacing-6); left: var(--md-sys-spacing-6); }
.m3e-snackbar-container--bottom-end   { bottom: var(--md-sys-spacing-6); right: var(--md-sys-spacing-6); }
.m3e-snackbar {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-4);
  min-width: 288px;
  max-width: 568px;
  padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
  background-color: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  /* D2-70: 28px, same rung as a dialog — the snackbar was the one sharp
     (4px) corner among 16px menus and 28px dialogs on the floating-surface
     ladder. */
  border-radius: var(--md-sys-shape-corner-extra-large);
  box-shadow: var(--md-sys-elevation-3);
  pointer-events: auto;
  /* Motion P1-4: transitions, not keyframes, so a dismiss during the entrance
     reverses from where the snackbar is instead of jumping. Enter keeps its
     timing and easing, now from @starting-style; exit is faster and uses
     --ease-drop. m3e-components.js removes the node on transitionend. */
  opacity: 1;
  transform: translateY(0) scale(1);
  transition:
    opacity var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-expressive-spatial-default),
    transform var(--md-sys-motion-duration-medium2) var(--md-sys-motion-easing-expressive-spatial-default);
}
@starting-style {
  .m3e-snackbar {
    opacity: 0;
    transform: translateY(16px) scale(0.95);
  }
}
.m3e-snackbar--exiting {
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  transition:
    opacity var(--duration-quick) var(--ease-drop),
    transform var(--duration-quick) var(--ease-drop);
}
.m3e-snackbar__message {
  flex: 1;
  font-size: 14px;
  line-height: 20px;
}
.m3e-snackbar__action {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.1px;
  color: var(--md-sys-color-inverse-primary);
  padding: 0;
  flex-shrink: 0;
  white-space: nowrap;
}
.m3e-snackbar__action {
  /* 44px target without a 44px visual pill: the padding does the work and the
     negative margin keeps the optical spacing the design asks for. */
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--md-sys-spacing-2);
  margin: calc(var(--md-sys-spacing-3) * -1) 0;
  border-radius: var(--md-sys-shape-corner-extra-small);
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) { .m3e-snackbar__action:hover { opacity: 0.8; } }
/* Focus: the house ring; `.m3e-snackbar :focus-visible` swaps it to inverse-primary. */

/* Dismiss control — present on every snackbar. Error snackbars never
   auto-dismiss, so this is the only way to close one. */
.m3e-snackbar__dismiss {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: calc(var(--md-sys-spacing-3) * -1) calc(var(--md-sys-spacing-2) * -1)
          calc(var(--md-sys-spacing-3) * -1) 0;
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
  opacity: 0.75;
  border-radius: var(--md-sys-shape-corner-full);
}
@media (hover: hover) and (pointer: fine) { .m3e-snackbar__dismiss:hover { opacity: 1; } }
.m3e-snackbar__dismiss svg { width: 18px; height: 18px; }


/* ═══════════════════════════════════════════════════════════════════════════
 * FAB (Floating Action Button)
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-fab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--md-sys-spacing-3);
  background-color: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
  border: none;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  text-decoration: none;
  transition:
    box-shadow var(--md-sys-motion-duration-short4) var(--easing-effects),
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
    transform var(--md-sys-motion-duration-short2) var(--easing-spatial);
}
/* The motion spec gives the FAB --easing-spatial; the press itself stays a
   small 0.97 so the spring reads as weight, not bounce (review P2-9). */
.m3e-fab:active {
  transform: scale(0.97);
}
.m3e-fab::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) { .m3e-fab:hover::before        { opacity: 0.08; } }
.m3e-fab:focus-visible::before { opacity: 0.12; }
.m3e-fab:active::before       { opacity: 0.16; }
.m3e-fab:focus-visible {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* Sizes */
.m3e-fab--small {
  width: 40px;
  height: 40px;
  border-radius: var(--md-sys-shape-corner-medium);
  box-shadow: var(--md-sys-elevation-3);
}
.m3e-fab--medium {
  width: 56px;
  height: 56px;
  border-radius: var(--md-sys-shape-corner-large);
  box-shadow: var(--md-sys-elevation-3);
}
.m3e-fab--large {
  width: 96px;
  height: 96px;
  border-radius: var(--md-sys-shape-corner-extra-large);
  box-shadow: var(--md-sys-elevation-3);
}
.m3e-fab--extended {
  height: 56px;
  padding: 0 var(--md-sys-spacing-5);
  border-radius: var(--md-sys-shape-corner-large);
  box-shadow: var(--md-sys-elevation-3);
  min-width: 80px;
}
@media (hover: hover) and (pointer: fine) { .m3e-fab:hover { box-shadow: var(--md-sys-elevation-4); } }

.m3e-fab__icon { width: 24px; height: 24px; flex-shrink: 0; }
.m3e-fab--small .m3e-fab__icon { width: 20px; height: 20px; }
.m3e-fab--large .m3e-fab__icon { width: 36px; height: 36px; }
.m3e-fab__label {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.1px;
  white-space: nowrap;
  overflow: hidden;
  max-width: 200px;
  /* The label fades; the width it occupies snaps (no max-width animation —
     review P2-7). */
  transition: opacity var(--md-sys-motion-duration-short3) var(--easing-effects);
}
.m3e-fab--extended.m3e-fab--collapsed .m3e-fab__label { max-width: 0; opacity: 0; }

/* Color variants */
.m3e-fab--secondary {
  background-color: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}
.m3e-fab--tertiary {
  background-color: var(--md-sys-color-tertiary-container);
  color: var(--md-sys-color-on-tertiary-container);
}
.m3e-fab--surface {
  background-color: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-primary);
}
.m3e-fab:disabled,
.m3e-fab[aria-disabled="true"] {
  background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
  color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
  box-shadow: none;
  transform: none;
  cursor: not-allowed;
}
.m3e-fab:disabled::before,
.m3e-fab[aria-disabled="true"]::before { display: none; }


/* ═══════════════════════════════════════════════════════════════════════════
 * ICON BUTTON
 *
 * `.m3e-icon-button` was used by four templates (the page-header "More
 * actions" button among them) but never defined, so it rendered as a bare
 * 20x20 glyph: under the 44px touch minimum, no hover, no pressed look, no
 * disabled look. One definition, every state: a 44px round hit area with the
 * house state layer (hover 8% / focus 12% / pressed 16% + a small press
 * scale), and the same disabled treatment as `.m3e-button`.
 * ═══════════════════════════════════════════════════════════════════════════ */
.m3e-icon-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: var(--md-sys-shape-corner-full, 9999px);
  background-color: transparent;
  color: var(--md-sys-color-on-surface-variant);
  cursor: pointer;
  overflow: hidden;
  transition: transform var(--md-sys-motion-duration-short2) var(--easing-effects);
}
.m3e-icon-button::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  opacity: 0;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-icon-button:hover::before         { opacity: 0.08; } }
.m3e-icon-button:focus-visible::before { opacity: 0.12; }
.m3e-icon-button:active::before        { opacity: 0.16; }
.m3e-icon-button[aria-expanded="true"]::before { opacity: 0.12; }
.m3e-icon-button:active { transform: scale(0.97); }
.m3e-icon-button:disabled,
.m3e-icon-button[aria-disabled="true"] {
  color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
  cursor: not-allowed;
  transform: none;
}
.m3e-icon-button:disabled::before,
.m3e-icon-button[aria-disabled="true"]::before { display: none; }


/* ═══════════════════════════════════════════════════════════════════════════
 * DIALOG
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── ONE SCRIM (A2 · Decision 5 · D1 System 9 / D1-33) ───────────────────────
 *
 * Three backdrop treatments shipped: the dialog and the bottom sheet at a flat
 * 32% dim, and the cancel-confirm at 18% dim + blur(8px) saturate(130%) set
 * inline from JS. The confirm's is the one D1 named the best-designed moment
 * in the surface, and two dialogs open on one page showed two different
 * backdrops behind them.
 *
 * It is declared here, once, and every overlay — dialog, bottom sheet, modal
 * nav drawer, confirm — inherits it. Only the geometry (which edge the panel
 * sits against, and which z-band it occupies) differs per component.
 *
 * `backdrop-filter` is progressive: where it is unsupported the dim alone
 * still reads as a scrim, which is why the dim is a real colour and not a
 * blur-only treatment. */
.m3e-scrim,
.m3e-dialog-scrim,
.m3e-bottom-sheet-scrim,
.m3e-nav-drawer-scrim {
  background-color: color-mix(in srgb, var(--md-sys-color-scrim) 18%, transparent);
  backdrop-filter: blur(8px) saturate(130%);
  -webkit-backdrop-filter: blur(8px) saturate(130%);
}
@media (prefers-reduced-transparency: reduce) {
  .m3e-scrim,
  .m3e-dialog-scrim,
  .m3e-bottom-sheet-scrim,
  .m3e-nav-drawer-scrim {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background-color: color-mix(in srgb, var(--md-sys-color-scrim) 42%, transparent);
  }
}

.m3e-dialog-scrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  align-items: center;
  justify-content: center;
}
.m3e-dialog-scrim[data-m3e-open] { display: flex; }

.m3e-dialog {
  background-color: var(--md-sys-color-surface-container-high);
  border-radius: var(--md-sys-shape-corner-extra-large);
  box-shadow: var(--md-sys-elevation-3);
  width: min(560px, calc(100vw - var(--md-sys-spacing-8)));
  max-height: min(560px, calc(100dvh - var(--md-sys-spacing-8)));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* The `.modal-enter` role (motion spec §6): 250ms from scale(0.95). */
  animation: modal-scale-in var(--md-sys-motion-duration-medium1) var(--easing-spatial);
}
/* Exit. closeOverlay() drops `data-m3e-open` at once and keeps the scrim
   painted with `m3e-overlay--closing` for the length of this animation — the
   old `:not([data-m3e-open])` exit rule never painted, because the scrim was
   already display:none. Faster than the entrance, `--ease-drop`. */
.m3e-dialog-scrim.m3e-overlay--closing {
  display: flex;
  pointer-events: none;
  animation: menu-backdrop-out var(--duration-quick) var(--ease-drop) forwards;
}
.m3e-overlay--closing .m3e-dialog {
  pointer-events: none;
  animation: modal-scale-out var(--duration-quick) var(--ease-drop) forwards;
}

.m3e-dialog__header {
  display: flex;
  flex-direction: column;
  gap: var(--md-sys-spacing-2);
  padding: var(--md-sys-spacing-6) var(--md-sys-spacing-6) var(--md-sys-spacing-4);
}
.m3e-dialog__icon {
  width: 24px;
  height: 24px;
  color: var(--md-sys-color-secondary);
  margin-bottom: var(--md-sys-spacing-2);
}
.m3e-dialog__title {
  font-size: 24px;
  font-weight: 400;
  color: var(--md-sys-color-on-surface);
  margin: 0;
  text-wrap: balance;
}
.m3e-dialog__content {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;   /* a fling at the end never scrolls the page */
  padding: 0 var(--md-sys-spacing-6) var(--md-sys-spacing-4);
  font-size: 14px;
  color: var(--md-sys-color-on-surface-variant);
  line-height: 20px;
}
.m3e-dialog__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--md-sys-spacing-2);
  padding: var(--md-sys-spacing-3) var(--md-sys-spacing-6);
}

/* Tall variant — for a dialog whose content is a long list rather than a
   short confirmation. The default cap is a flat 560px, so on a 900px viewport
   the block confirmation dialog squeezed 825px of checklist into a 376px
   window and scrolled it inside two more scrollers (2026-09-11 mobile audit,
   P2-3). Opt in per dialog; the 560px default stays right for "Are you sure?".
   Viewport-relative so it grows with the screen instead of ignoring it, and
   capped so it does not become an unreadable full-height column on a 4K
   monitor. */
.m3e-dialog--tall {
  max-height: min(840px, calc(100dvh - var(--md-sys-spacing-8)));
}
/* …but not over the phone tab bar. The 560px default happened to clear it on
   a 844px phone; a viewport-tall dialog does not, and while the scrim outranks
   the bar (z 1200 vs 40) so the action row still takes the tap, putting
   "Record as held" exactly where Home and Approvals live is not a thing to ask
   of a thumb. Below the bottom-nav breakpoint, reserve the bar's height.
   The scrim centres its dialog, so shrinking alone would not move it clear —
   the matching `margin-bottom` is what lifts it, and the two together leave
   the same 16px gap at the top that the spacing-8 cap gives elsewhere. */
@media (max-width: 839px) {
  .m3e-dialog--tall {
    max-height: calc(
      100dvh - var(--m3e-bottom-nav-height) - env(safe-area-inset-bottom, 0px)
             - var(--md-sys-spacing-8)
    );
    margin-bottom: calc(var(--m3e-bottom-nav-height) + env(safe-area-inset-bottom, 0px));
  }
}

/* Full-screen variant */
.m3e-dialog--fullscreen {
  width: 100vw;
  max-width: 100vw;
  /* dvh: `vh` includes the collapsed browser toolbar on phones, so the action
     row sat under it. The vh line is the fallback for engines without dvh. */
  height: 100vh;
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  border-radius: 0;
  margin: 0;
}
.m3e-dialog--fullscreen .m3e-dialog__header {
  flex-direction: row;
  align-items: center;
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
  padding-block: var(--md-sys-spacing-3);
}
.m3e-dialog--fullscreen .m3e-dialog__title { flex: 1; font-size: 22px; }


/* ═══════════════════════════════════════════════════════════════════════════
 * BOTTOM SHEET
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-bottom-sheet-scrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: var(--z-sheet);
  align-items: flex-end;
}
.m3e-bottom-sheet-scrim[data-m3e-open] { display: flex; }
.m3e-bottom-sheet-scrim--standard {
  background-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  pointer-events: none;
}
.m3e-bottom-sheet-scrim--standard[data-m3e-open] { pointer-events: auto; }

.m3e-bottom-sheet {
  width: 100%;
  max-height: 90vh;
  max-height: 90dvh;
  background-color: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-extra-large) var(--md-sys-shape-corner-extra-large) 0 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  pointer-events: auto;
  will-change: transform;
  /* Edge-anchored travel uses the standard spatial curve: the expressive one
     overshoots ~1.4%, lifting the sheet off the bottom edge and showing page
     under it. The snap-back after a short drag uses the same curve. */
  animation: m3e-sheet-enter var(--duration-med)
             var(--md-sys-motion-easing-standard-spatial-default);
  transition: transform var(--duration-med)
              var(--md-sys-motion-easing-standard-spatial-default);
}
@keyframes m3e-sheet-enter {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
/* Exit (closeOverlay → `m3e-overlay--closing` on the scrim): the scrim fades
   and the sheet drops, both faster than the entrance, on `--ease-drop`. A
   drag-dismissed sheet (`data-m3e-drag-exit`) is already being carried down
   by an inline transition from where the finger let go; restarting the
   keyframe from translateY(0) would snap it back up first. */
.m3e-bottom-sheet-scrim.m3e-overlay--closing {
  display: flex;
  pointer-events: none;
  animation: menu-backdrop-out var(--duration-quick) var(--ease-drop) forwards;
}
.m3e-overlay--closing .m3e-bottom-sheet { pointer-events: none; }
.m3e-overlay--closing .m3e-bottom-sheet:not([data-m3e-drag-exit]) {
  animation: m3e-sheet-exit var(--duration-quick) var(--ease-drop) forwards;
}
@media (min-width: 840px) {
  /* The desktop-dialog variant is a centred dialog at this width: it leaves
     like one. */
  .m3e-overlay--closing .m3e-bottom-sheet.m3e-sheet--desktop-dialog:not([data-m3e-drag-exit]) {
    animation: modal-scale-out var(--duration-quick) var(--ease-drop) forwards;
  }
}
@keyframes m3e-sheet-exit {
  from { transform: translateY(0); }
  to   { transform: translateY(100%); }
}
.m3e-bottom-sheet__drag-handle-wrapper {
  display: flex;
  justify-content: center;
  padding: var(--md-sys-spacing-2) 0;
  cursor: grab;
}
.m3e-bottom-sheet__drag-handle-wrapper:active { cursor: grabbing; }
.m3e-bottom-sheet__drag-handle {
  background: none;
  border: none;
  padding: var(--md-sys-spacing-2) var(--md-sys-spacing-8);
  cursor: grab;
}
.m3e-bottom-sheet__drag-handle::after {
  content: '';
  display: block;
  width: 32px;
  height: 4px;
  border-radius: 2px;
  background-color: var(--md-sys-color-on-surface-variant);
  opacity: 0.4;
}
.m3e-bottom-sheet__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--md-sys-spacing-3) var(--md-sys-spacing-6);
}
/* The header is a grab area too (m3e-components.js §9), not just the 36px
   handle strip. The browser must not claim the vertical pan for scrolling. */
[data-m3e-draggable] .m3e-bottom-sheet__drag-handle-wrapper,
[data-m3e-draggable] .m3e-bottom-sheet__header {
  touch-action: none;
}
.m3e-bottom-sheet__title {
  font-size: 16px;
  font-weight: 500;
  color: var(--md-sys-color-on-surface);
  text-wrap: balance;
}
.m3e-bottom-sheet__content {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;   /* a fling at the end never scrolls the page */
  padding: 0 var(--md-sys-spacing-6) var(--md-sys-spacing-6);
  /* Clear the home indicator: `viewport-fit=cover` puts the sheet's last row
     under the gesture bar otherwise. */
  padding-bottom: max(var(--md-sys-spacing-6),
                      calc(env(safe-area-inset-bottom, 0px) + var(--md-sys-spacing-4)));
}
.m3e-bottom-sheet--full-height { max-height: 100vh; max-height: 100dvh; }


/* ═══════════════════════════════════════════════════════════════════════════
 * Shared: Scrim close button (data-m3e-trigger on scrim)
 * ═══════════════════════════════════════════════════════════════════════════ */

/* Scrim divs that are the trigger for closing an overlay */
.m3e-dialog-scrim[data-m3e-trigger],
.m3e-bottom-sheet-scrim[data-m3e-trigger],
.m3e-nav-drawer-scrim[data-m3e-trigger] {
  cursor: default;
}
/* Prevent click-through to content from triggering scrim close */
.m3e-dialog,
.m3e-bottom-sheet {
  pointer-events: auto;
}
/* Stop scrim close event from reaching the content */
.m3e-dialog,
.m3e-bottom-sheet { isolation: isolate; }

/* ═══════════════════════════════════════════════════════════════════════════
   Shell Layout — Phase 4
   ═══════════════════════════════════════════════════════════════════════════ */

.m3e-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;   /* MN-09: vh is the toolbar-collapsed height */
}

.m3e-shell__body {
  display: flex;
  flex: 1;
}

/* Permanent drawer (committee/auditor desktop sidebar) */
.m3e-shell__drawer--permanent {
  position: fixed;
  top: var(--shell-top-bar-height);
  left: 0;
  bottom: 0;
  /* One shell, two densities. The committee drawer carries sixteen grouped
     destinations and needs 240px; the auditor's carries six flat ones and
     reads as an over-wide column at the same width. */
  width: var(--shell-drawer-width, 240px);
  z-index: var(--z-banner);
  overflow-x: hidden;
  /* Q4-02: the SHELL slot no longer scrolls. `.m3e-nav-drawer__body` inside it
     does, so the sign-out footer stays pinned below the rows instead of riding
     1082px down a scrolling column, and the drawer's own scrollbar belongs to
     the list of destinations rather than to the whole panel. */
  overflow-y: hidden;
  padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
  display: none;
}

/* Main content area */
.m3e-shell__content {
  flex: 1;
  padding-top: var(--shell-top-bar-height);
  min-width: 0;
}
/* L7-21: a descendant with a long unbreakable token (a 44-character audit hash
   in a `dd.npsc-tnum`, an event key in a `div.font-mono`) propagates its
   min-content width all the way up, and the WHOLE PAGE scrolls sideways —
   464px of document in a 386px viewport on the auditor invoice detail. Two
   guards, stated once, for every surface. */
.m3e-shell__content > * { min-width: 0; }
.npsc-tnum,
.font-mono { overflow-wrap: anywhere; }
/* F-07 / F-19: an email address is one unbreakable token, so in any narrow box
   — a booker row 142px wide, a member's account line on a 386px cover screen —
   it either overflowed its cell or stopped mid-domain with no way to read the
   rest. It is allowed to wrap wherever it must: a wrapped address is whole and
   readable, a clipped one is neither. One class, every surface that prints an
   address. */
.npsc-email { overflow-wrap: anywhere; }

/* Bottom navigation bar (mobile) */
.m3e-shell__bottom-nav {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: var(--z-chrome);
}

/* Bottom padding to prevent content being hidden behind fixed bottom nav.
   Uses the same 840px breakpoint where the bottom nav hides. */
.m3e-shell__content--has-bottom-nav {
  padding-bottom: calc(6rem + env(safe-area-inset-bottom, 0px));
}


/* Desktop member tab bar (hidden on mobile) */
.m3e-shell__nav-tabs {
  display: none;
  /* L7-10 / L1-28: six links 4px apart, each with an 8px-radius wash, read as
     one ragged bar when the pointer travels the row. 8px is the law (§2). */
  gap: var(--md-sys-spacing-2);
  /* L1-26 / L2-04: the nav is what gives way when the bar runs out of room,
     never the account control at the end of it. Deliberately NOT
     `overflow: hidden` — that would hide a destination rather than reveal the
     squeeze, and it would shear the focus ring off the first and last link. */
  min-width: 0;
}

@media (min-width: 840px) {
  .m3e-shell__body {
    width: 100%;
    max-width: 1440px;
    margin-left: auto;
    margin-right: auto;
  }
  .m3e-shell__drawer--permanent {
    /* Q4-02: this said `display: block`, on the SAME element that carries
       `.m3e-nav-drawer` (`display: flex; flex-direction: column`) - so the
       permanent drawer was never a flex column at all. That is why the footer
       had to be `position: sticky` to stay in view, and why it painted over the
       last two destinations. As a real column the body scrolls and the footer
       simply sits below it. */
    display: flex;
    flex-direction: column;
    position: sticky;
    top: var(--shell-top-bar-height);
    left: auto;
    bottom: auto;
    height: calc(100vh - var(--shell-top-bar-height));
    height: calc(100dvh - var(--shell-top-bar-height));
    flex-shrink: 0;
  }
  /* Center top bar content to match shell body width */
  .m3e-top-app-bar--with-drawer > * {
    max-width: 1440px;
    margin-left: auto;
    margin-right: auto;
  }
  .m3e-shell__bottom-nav {
    display: none;
  }
  .m3e-shell__content--has-bottom-nav {
    padding-bottom: 1.5rem;
  }
  .m3e-shell__nav-tabs {
    display: flex;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Navigation RAIL — 840px to 1023px (A2 · D2-07)

   The permanent drawer is 240px at every width from 840 upwards. At 1440 that
   is 17% of the screen and correct; at 860 — a folded inner display, a small
   laptop — it is 28%, and the working column that remains is 562px. The rail
   is the M3 answer for exactly this window: 80px, icon plus the active label,
   group labels dropped, the label carried by `title`/`aria-label`.

   It is the SAME element, restyled, not a second navigation. Rendering
   `m3e/components/navigation_rail.html` alongside the drawer would put a
   second `#main-nav`, a second `#desktop-nav-search` and a second copy of
   every `data-nav-id` in the document — which is what `m3e-shell.js` and
   `core/tests_nav_integrity.py` both read to decide the active destination.
   One nav in the DOM, two presentations.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 840px) and (max-width: 1023px) {
  :root { --shell-drawer-width: 80px; }

  .m3e-nav-drawer--permanent .m3e-nav-drawer__item {
    justify-content: center;
    padding-inline: 0;
    margin-inline: var(--md-sys-spacing-2);
    gap: 0;
  }
  .m3e-nav-drawer--permanent .m3e-nav-drawer__item--child {
    padding-left: 0;
  }
  /* The label still exists for assistive tech and for quick-find's text
     match; it is the ICON that names the destination at this width. */
  .m3e-nav-drawer--permanent .m3e-nav-drawer__item-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .m3e-nav-drawer--permanent .m3e-nav-drawer__item-icon { opacity: 1; }
  /* A count nobody can read is worse than no count: at 80px the badge becomes
     a dot on the icon's shoulder, and the accessible name still says the
     number. */
  .m3e-nav-drawer--permanent .m3e-nav-drawer__item .m3e-badge {
    position: absolute;
    top: 4px;
    right: 14px;
    min-width: 8px;
    width: 8px;
    height: 8px;
    padding: 0;
    font-size: 0;
    line-height: 0;
    overflow: hidden;
  }
  /* Group labels are the first thing to go — five overlines in an 80px column
     would each wrap to three lines. The groups are still in the More sheet and
     in the mobile drawer, where there is room to read them. */
  .m3e-nav-drawer--permanent .m3e-nav-drawer__section-header { display: none; }
  /* Quick-find becomes its own icon-sized affordance rather than a search box
     that cannot show what was typed. */
  .m3e-nav-drawer--permanent .m3e-nav-drawer__quick-find-wrap { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   KPI stat-card scroll strip (mobile → horizontal scroll, sm+ → grid)
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 639px) {
  .m3e-kpi-strip {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
  }
  .m3e-kpi-strip > * {
    flex: 0 0 65vw;
    scroll-snap-align: start;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   Global Scrollbar — M3E thin, tinted scrollbar
   ═══════════════════════════════════════════════════════════════════════════ */

/* Firefox / modern browsers */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--md-sys-color-outline-variant) transparent;
}

/* WebKit (Chrome, Safari, Edge) */
::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-full);
}
::-webkit-scrollbar-thumb:hover {
  background: var(--md-sys-color-outline);
}
/* Hide scrollbar on very small elements (tabs, horizontal strips) */
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { scrollbar-width: none; }


/* ═══════════════════════════════════════════════════════════════════════════
 * EMPTY STATE
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: var(--md-sys-spacing-16) var(--md-sys-spacing-4);
  text-align: center;
}
.m3e-empty-state__icon {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 80px;
  height: 80px;
  /* L3-25 (B3): `horizontal_overflow` flagged this on every page that renders
     an empty state, because the dashed ring below used to be a `::before` at
     `inset: -6px` — 6px of scrollable overflow on an 80px box. An `outline`
     draws the same ring and never participates in layout or scroll geometry.
     See the rule below. */
  flex: none;
  border-radius: var(--md-sys-shape-corner-full);
  background-color: color-mix(in srgb, var(--md-sys-color-primary-container) 55%, var(--md-sys-color-surface-container));
  color: var(--md-sys-color-on-primary-container);
}
/* The ring, as an outline rather than an overflowing pseudo-element
   (L3-25). `outline` follows `border-radius` in every browser this product
   supports and costs the box no scrollable overflow.

   A6a (B4 request 5): it used to be DASHED. A dashed ring means "drop
   something here" everywhere else in this product (see the drop zone), and
   nothing can be dropped into an empty state. Solid. */
.m3e-empty-state__icon {
  outline: 1.5px solid color-mix(in srgb, var(--md-sys-color-outline-variant) 60%, transparent);
  outline-offset: 6px;
}
.m3e-empty-state__icon svg {
  width: 36px;
  height: 36px;
}
.m3e-empty-state__icon--illustration svg {
  width: 100%;
  height: 100%;
}
/* Phones: cap the decoration so the message and its action stay above the fold.
   A 128px illustration on a 390x844 screen pushes the one thing the member is
   supposed to do next off-screen. */
@media (max-width: 839px) {
  .m3e-empty-state__icon,
  .m3e-empty-state__icon--illustration {
    width: 96px;
    height: 96px;
  }
}
.m3e-empty-state__heading {
  margin-top: var(--md-sys-spacing-5);
  font-size: var(--md-sys-typescale-title-large-size, 1.375rem);
  font-weight: 500;
  color: var(--md-sys-color-on-surface);
}
.m3e-empty-state__description {
  margin-top: var(--md-sys-spacing-2);
  font-size: 14px;
  color: var(--md-sys-color-on-surface-variant);
  /* D4-25: prose reads best under ~68 characters per line regardless of the
     container it sits in. */
  max-width: 68ch;
}

/* D4-25 / A3 residual: a full-page empty state (a whole tab, a whole
   register with no rows at all) can afford the roomy, floating-icon
   treatment above. One that sits INSIDE a card — a KPI panel, a "this
   month" rail, a filtered-to-nothing results card — is a small region of a
   bigger page, not the whole point of the screen, and the full treatment
   measured ~390px on the reports page: taller than the card's siblings.
   Capped here to roughly 180px for the common case (icon + heading + one
   line of description, no CTA); a call site that adds a button will run a
   little taller, which is expected.

   A6a (B3 request 5): the same recipe is now also reachable by NAME —
   `size="compact"` on the partial emits `.m3e-empty-state--compact`. B3 could
   not use the descendant rule on the reports page, the year-end page or the
   invoice payment-matching panel because those three regions are not
   `.m3e-card` descendants, so they hand-rolled one sentence instead of using
   the component. The two selectors share every declaration below. */
.m3e-card .m3e-empty-state,
.m3e-empty-state--compact {
  padding: var(--md-sys-spacing-6) var(--md-sys-spacing-4);
}
.m3e-card .m3e-empty-state .m3e-empty-state__icon,
.m3e-empty-state--compact .m3e-empty-state__icon {
  width: 48px;
  height: 48px;
  outline-offset: 4px;
}
.m3e-card .m3e-empty-state .m3e-empty-state__icon svg,
.m3e-empty-state--compact .m3e-empty-state__icon svg {
  width: 24px;
  height: 24px;
}
.m3e-card .m3e-empty-state .m3e-empty-state__heading,
.m3e-empty-state--compact .m3e-empty-state__heading {
  margin-top: var(--md-sys-spacing-4);
  font-size: var(--md-sys-typescale-title-medium-size, 1rem);
}
.m3e-card .m3e-empty-state .m3e-empty-state__description,
.m3e-empty-state--compact .m3e-empty-state__description {
  margin-top: var(--md-sys-spacing-1);
}
/* The phone cap must not fight the compact size: `--illustration` sets 96px in
   the 839px media query, which is larger than the compact 48px. */
@media (max-width: 839px) {
  .m3e-card .m3e-empty-state .m3e-empty-state__icon,
  .m3e-card .m3e-empty-state .m3e-empty-state__icon--illustration,
  .m3e-empty-state--compact .m3e-empty-state__icon,
  .m3e-empty-state--compact .m3e-empty-state__icon--illustration {
    width: 48px;
    height: 48px;
  }
}


/* FOCUS RING: deliberately static. Keyboard-initiated focus is never
   animated (review P1-6); the ring appears in one frame. */

/* ═══════════════════════════════════════════════════════════════════════════
 * TOM SELECT OVERRIDES
 * Ensure TomSelect wrappers fill their parent container width.
 * ═══════════════════════════════════════════════════════════════════════════ */

.ts-wrapper {
  width: 100%;
  text-transform: none;
  letter-spacing: normal;
}
/* The wrapper IS the house control box (see the control rule at the top of
   this file); it centres the transparent text line the .ts-control provides. */
.ts-wrapper.single {
  display: flex;
  align-items: center;
  cursor: pointer;
}
.ts-wrapper.single > .ts-control { flex: 1 1 auto; min-width: 0; }

/* Make TomSelect single controls visually match native M3E selects.
   The outer wrapper already carries border/radius/padding classes from the form
   field, so the inner TomSelect control should be neutral. */
.ts-wrapper.single .ts-control,
.ts-wrapper.single.input-active .ts-control,
.ts-wrapper.single.has-items .ts-control,
.ts-wrapper.single.dropdown-active .ts-control {
  background: transparent;
  border: 0;
  box-shadow: none;
  min-height: 1.25em;
  padding: 0 !important;
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  white-space: nowrap;
  overflow: hidden;
}

.ts-wrapper.single .ts-control > .item {
  background: transparent;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  margin: 0 !important;
  padding: 0 !important;
  color: inherit;
  font: inherit;
  line-height: inherit;
  flex: 0 0 auto;
  white-space: nowrap;
}

.ts-wrapper.single .ts-control > input {
  margin: 0 !important;
  min-width: 0 !important;
  width: 0 !important;
  flex: 1 1 auto;
  padding: 0 !important;
  border: 0 !important;
  box-shadow: none !important;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: inherit;
}

/* D5-03: the comment that used to sit here asserted that "the wrapper carries
   `.m3e-select`'s surface, border and radius" and that the measured height was
   44px. It did not and it was not — the wrapper is a plain `div` that inherits
   nothing unless a template happens to add the class, and only 1 of 7 observed
   wrappers did, so the combobox shipped as a third geometry (41.6px / 4px /
   `--border` / transparent, and 48.4px on phones, where the gap with the
   select changed DIRECTION). `.ts-wrapper.single` is now in the house control
   rule at the top of this file, so it IS the box, by construction.
   D5-12: the density step is scoped to the SURFACE, not the breakpoint —
   `--control-fs` is 14px in committee/auditor work areas at ≥840px and 16px on
   member and public at every width. A member on a laptop no longer gets a
   14px combobox in a 16px form. */
.ts-wrapper.single .ts-control,
.ts-wrapper.single .ts-control input,
.ts-wrapper.single .ts-control > .item {
  /* The vendor foreground is fixed dark ink, including in the dark theme. */
  color: var(--fg1);
  font-size: var(--control-fs);
  line-height: 1.4;
}
.ts-wrapper.single .ts-control input::placeholder {
  color: var(--md-sys-color-on-surface-variant);
  opacity: 1;
}

/* When a value is selected, keep only a tiny caret-width input so it doesn't
   wrap onto a new line under the selected item. */
.ts-wrapper.single.has-items .ts-control > input {
  width: 2px !important;
  min-width: 2px !important;
  flex: 0 0 2px;
  margin-left: 2px !important;
}

/* Prevent label uppercase/tracking from leaking into form controls */
label.uppercase input,
label.uppercase select,
label.uppercase textarea,
label.uppercase .ts-wrapper {
  text-transform: none;
  letter-spacing: normal;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * SEGMENTED CONTROL
 * Outer container + inner pill items using M3E shape tokens.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
 * SEMANTIC STATUS UTILITIES
 * Map domain statuses to M3E color token families for theme-aware styling.
 *
 * Background + text pairs:
 *   .m3e-status-success   — tertiary-container  (teal family)
 *   .m3e-status-error     — error-container     (red family)
 *   .m3e-status-warning   — --warning-*         (court-line yellow)
 *   .m3e-status-info      — primary-container   (blue family)
 *   .m3e-status-neutral   — surface-container   (neutral family)
 *
 * Text-only variants:
 *   .m3e-on-success, .m3e-on-error, .m3e-on-warning, .m3e-on-info
 * ═══════════════════════════════════════════════════════════════════════════ */

/* Status background + text pairs */
.m3e-status-success { background: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container); }
.m3e-status-error   { background: var(--md-sys-color-error-container);    color: var(--md-sys-color-on-error-container); }
.m3e-status-warning { background: var(--warning-container); color: var(--warning-on-container); }
.m3e-status-info    { background: var(--md-sys-color-primary-container);  color: var(--md-sys-color-on-primary-container); }
.m3e-status-neutral { background: var(--md-sys-color-surface-container);  color: var(--md-sys-color-on-surface-variant); }

/* Text-only status colors */
.m3e-on-success { color: var(--md-sys-color-on-tertiary-container); }
.m3e-on-error   { color: var(--md-sys-color-error); }
.m3e-on-warning { color: var(--warning-on-container); }
.m3e-on-info    { color: var(--md-sys-color-on-primary-container); }


/* ═══════════════════════════════════════════════════════════════════════════
 * SEGMENTED CONTROL
 * Outer container + inner pill items using M3E shape tokens.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-seg-container {
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container);
}
.m3e-seg-pill {
  border-radius: var(--md-sys-shape-corner-small);
}

/* Segmented-control geometry — any [data-seg-control] toggle owned by the shell
   repositioner. Buttons keep a 40px visual height on all viewports and gain a
   ::after hit-area extender to ≥44px effective, mirroring the chip fix. Buttons
   own a stacking context so the negative-z extender never drops below siblings. */
[data-seg-control] button,
[data-seg-control] a,
[data-portal-tab] {
  position: relative;
  z-index: 0;
  min-height: 40px;
}
[data-seg-control] button::after,
[data-seg-control] a::after,
[data-portal-tab]::after {
  content: '';
  position: absolute;
  inset: -2px 0;   /* 40 + 2×2 = 44px effective target */
  z-index: -1;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * M3E VISUAL POLISH
 * Award-winning aesthetic layer — builds on top of existing M3E system.
 * All colors via --md-sys-color-* tokens; works in light + dark + HC modes.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* Section header with left primary accent bar */
.m3e-section-header {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  padding: var(--md-sys-spacing-2) 0;
}
.m3e-section-header::before {
  content: '';
  display: block;
  width: 3px;
  height: 1.25em;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary);
  flex-shrink: 0;
}

/* KPI card top accent — set --kpi-accent on the element */
.m3e-card--accent-top {
  border-top: 3px solid var(--kpi-accent, var(--md-sys-color-primary));
}



/* ═══════════════════════════════════════════════════════════════════════════
 * Shell & Page Utilities (extracted from base.html)
 *
 * Companion keyframes live in motion.css.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Color-scheme: tell the browser whether we're light or dark so native
   controls (selects, checkboxes, scrollbars, date pickers) render correctly. ── */
:root { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) { color-scheme: dark; }
}

/* ── Tokenised hover utility ── */
/* L7-11: the state layer, not an opaque surface swap — so it can sit over a
   coloured row, follows high contrast, and fades rather than snapping. */
.hover-fill { position: relative; isolation: isolate; }
.hover-fill::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .hover-fill:hover::before         { opacity: 0.08; } }
.hover-fill:focus-visible::before { opacity: 0.12; }
.hover-fill:active::before        { opacity: 0.16; }

/* ── HTMX loading indicators ── */
/* ── Skeleton shimmer ── */
.m3e-skeleton {
  background: linear-gradient(
    90deg,
    var(--md-sys-color-surface-container) 25%,
    var(--md-sys-color-surface-container-high) 50%,
    var(--md-sys-color-surface-container) 75%
  );
  background-size: 200% 100%;
  animation: m3e-shimmer var(--md-sys-motion-duration-shimmer) var(--easing-effects) infinite;
  border-radius: var(--md-sys-shape-corner-small);
}
.m3e-skeleton--text { height: 14px; margin-bottom: 8px; }
.m3e-skeleton--title { height: 22px; width: 60%; margin-bottom: 12px; }
.m3e-skeleton--card { height: 72px; border-radius: var(--md-sys-shape-corner-medium); }
/* `table` and `list` variants of core/_partials/_skeleton.html. */
.m3e-skeleton--row { height: 40px; margin-bottom: 8px; border-radius: var(--md-sys-shape-corner-small); }
.m3e-skeleton--avatar {
  width: 40px;
  height: 40px;
  border-radius: var(--md-sys-shape-corner-full);
  flex-shrink: 0;
}
.m3e-skeleton-listrow {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  padding: var(--md-sys-spacing-3) 0;
}
.m3e-skeleton-listrow__lines { flex: 1; min-width: 0; }
.m3e-skeleton-listrow__lines .m3e-skeleton--text:last-child { margin-bottom: 0; }
.m3e-skeleton--circle { border-radius: var(--md-sys-shape-corner-full); }
@keyframes m3e-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* A8-60: the bar used to cross-fade in over 200ms. A local navigation lands
   long before that, so what the user actually saw was an ~11%-opacity flicker
   — worse than nothing, because it reads as a glitch rather than as progress.
   The show direction now waits 140ms before it starts: a navigation faster
   than that never paints the bar at all, and a slower one gets a bar at full
   strength almost at once. Hiding stays immediate — a spinner that lingers
   after the content has arrived is its own kind of lie. */
.htmx-indicator { opacity: 0; transition: opacity var(--md-sys-motion-duration-short2) linear; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
  transition: opacity var(--md-sys-motion-duration-short2) linear var(--md-sys-motion-delay-indicator);
}
/* The global navigation bar (base.html). One motion only — the traverse;
   the old opacity pulse stacked a second infinite animation on a 3px bar
   (review P2-14). */
.m3e-page-loading-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: 3px;
  background: var(--gradient-loading);
  box-shadow: 0 0 8px var(--brand);
  pointer-events: none;
}
.htmx-request #page-loading-bar,
.htmx-request#page-loading-bar {
  animation: loading-slide var(--md-sys-motion-duration-loader-cycle) var(--easing-effects) infinite;
}
/* Real progress (m3e-motion.js, htmx:xhr:progress with a known length): the
   traverse stops and the bar fills from the left by scaleX. */
#page-loading-bar.m3e-page-loading-bar--determinate {
  animation: none;
  transform-origin: left center;
  transform: scaleX(var(--m3e-load-progress, 0));
  /* Keeps the indicator's show delay (A8-60): a fast navigation still never
     paints the bar. */
  transition:
    opacity var(--md-sys-motion-duration-short2) linear var(--md-sys-motion-delay-indicator),
    transform var(--duration-quick) var(--easing-effects);
}
#page-loading-bar.m3e-page-loading-bar--starting { transition-property: opacity; }

/* ── Link utility classes ── */
.link-inline {
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  text-underline-offset: 0.25rem;
}
@media (hover: hover) and (pointer: fine) { .link-inline:hover { text-decoration-color: currentColor; } }
@media (hover: hover) and (pointer: fine) { .link-list-item:hover { text-decoration: underline; text-underline-offset: 0.125rem; } }

/* ── Global focus states ── */
a:focus-visible { border-radius: 0.125rem; } /* outline: the house ring (P2-16) */
/* .bg-slate-9xx links: ring colour swap lives with the house ring. */
/* Size the focused skip-link box to its text so it doesn't overflow. */
.skip-link { width: max-content; max-width: calc(100vw - 2rem); }
.skip-link:focus-visible { outline: 3px solid var(--md-sys-color-primary); outline-offset: 2px; }

/* ── Card separator ── */
.card-separator { margin-left: 1rem; height: 0.5px; background: var(--md-sys-color-outline-variant); }

/* ── Nav active / inactive states ── */
.nav-active svg { color: var(--md-sys-color-on-secondary-container) !important; }
.nav-active .m3e-nav-bar__label { color: var(--md-sys-color-on-secondary-container) !important; font-weight: 600; }
.nav-active .nav-indicator { opacity: 1 !important; background: var(--md-sys-color-primary); }
.nav-inactive svg { color: var(--md-sys-color-on-surface-variant) !important; }
.nav-inactive .m3e-nav-bar__label { color: var(--md-sys-color-on-surface-variant) !important; }
.nav-inactive .nav-indicator { opacity: 0 !important; }
/* L7-10 / L1-28: the opaque `--surface-container` swap became the shared state
   layer, so the hovered link and the active pill stop reading as one two-tone
   shape. `.m3e-shell__nav-tabs` carries the 8px gap (§2 law 4). */
.member-top-nav-link {
  color: var(--md-sys-color-on-surface-variant);
  /* L1-27: `py-2.5` on a 15px/1.2 line box lands at 43px — one pixel under the
     floor, and at 840-1024px (a Fold in landscape) these are real touch
     targets. Stated here rather than as a utility, so no call site can lose
     it. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  position: relative;
  isolation: isolate;
  transition: color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.member-top-nav-link::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .member-top-nav-link:hover::before        { opacity: 0.08; } }
.member-top-nav-link:focus-visible::before { opacity: 0.12; }
.member-top-nav-link:active::before       { opacity: 0.16; }
@media (hover: hover) and (pointer: fine) { .member-top-nav-link:hover:not(.is-active) { color: var(--md-sys-color-on-surface); } }
.member-top-nav-link.is-active {
  /* Court & Print budget: the active page marker is location, not action.
   * Warm secondary-container keeps the world's floor character while
   * court red is spent on the page's one primary action. */
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  font-weight: 600;
}

/* ── Sidebar nav ── */
/* L7-09 / L7-11: `.sidebar-link` sits on the SAME elements as
   `.m3e-nav-drawer__item`, and it used to paint a second, different hover
   (an opaque surface step) with an untokenised 150ms transition — two hover
   systems on one element, decided by source order. It is now a pure layout
   alias; the drawer row owns the paint. */
.sidebar-link {
  display: flex; align-items: center; gap: 0.75rem;
  border-radius: var(--md-sys-shape-corner-small);
  padding: 0.5rem 0.75rem;
  color: var(--md-sys-color-on-surface);
}
.sidebar-section-label {
  padding: 0.5rem 0.75rem; font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--md-sys-color-on-surface-variant);
}

/* ── Table interactive rows ── */
/* L7-07: one wash. This used to paint a SECOND, different hover
   (`--surface-container`, a solid surface step) over the same row the
   `.m3e-table` rule already washed. The `.m3e-table` rule is the wash; this
   only tokenises the duration the file's motion spec requires. */
.table-row-interactive {
  transition: background-color var(--md-sys-motion-duration-short1)
              var(--md-sys-motion-easing-standard-effects-default);
}

/* ── Navigation loading dim ── */
/* The opacity transition lives on the base `#page-content` rule (motion.css)
   so the dim eases in AND out; a transition only on this rule snapped back. */
#page-content.nav-loading {
  opacity: 0.55;
  pointer-events: none;
}

/* ── Desktop hover states (hover-capable devices only) ── */
/* L5-35: `.table-row-danger` was inside `@media (hover: hover)`, so an overdue
   invoice lost its red wash on every touch device — the one place the flag
   matters most is the phone the treasurer chases arrears on. It is a STATE,
   not a hover. */
/* A3 / D4-09 — a flag, not a wall.
 *
 * This washed the whole row in `error-container`. Two things broke. (1) The
 * status badge in that row resolved to the same token, so on an overdue
 * invoice the one element whose job is to be a chip stopped being one — bare
 * coloured text on its own colour, in both themes. (2) In dark,
 * `error-container` is a fully saturated brick, so the default invoice view
 * was a 600px block of solid red with brand-blue links on top of it, and the
 * "danger" signal covered 100% of the table, which is the same as covering
 * none of it.
 *
 * An 8% wash keeps the row scannable down the page without becoming the page.
 * The badge names the state without a duplicate side stripe (C2-14; see the
 * `.table-row-danger .m3e-badge--error-container` rule in the BADGE section). */
.m3e-table tbody tr.table-row-danger,
.table-row-danger {
  background: color-mix(in srgb, var(--md-sys-color-error) 8%, transparent);
}

/* ── View Transitions ── */
/* Opt in only when the reader has not asked for reduced motion; otherwise a
   page change is an instant swap and no transition is ever started. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
::view-transition-old(root) {
  animation: var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard-effects-default) vt-fade-out;
}
::view-transition-new(root) {
  animation: var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-expressive-spatial-default) vt-fade-in;
}

/* ── Sheet / modal animation classes (motion spec §6) ──
 * Open with spatial confidence; close is ALWAYS faster and uses --ease-drop
 * (decisive dismissal). Tokens only — no magic ms, no bespoke curves. */
.sheet-enter { animation: sheet-slide-up var(--duration-med) var(--easing-spatial) forwards; }   /* 300ms */
.sheet-exit  { animation: sheet-slide-down var(--duration-quick) var(--ease-drop) forwards; }     /* 200ms */
.modal-enter { animation: modal-scale-in var(--md-sys-motion-duration-medium1) var(--easing-spatial) forwards; } /* 250ms */
.modal-exit  { animation: modal-scale-out var(--duration-quick) var(--ease-drop) forwards; }      /* 200ms */
/* Scrim: fade in on open, faster fade out on close (motion spec §6). */
.m3e-confirm-scrim-in  { animation: menu-backdrop-in var(--duration-quick) var(--easing-effects) forwards; }
.m3e-confirm-scrim-out { animation: menu-backdrop-out var(--duration-quick) var(--ease-drop) forwards; }

/* The confirm service's panel (m3e-confirm.js), emil P2-4. It borrows the
   dialog component's surface, corner and elevation so a confirm and a dialog
   on one page read as one system; only stacking geometry stays inline in JS.
   The accept tone is `.m3e-button--error`, so `.m3e-button:disabled` paints
   the typed-word gate. */
.m3e-confirm-panel {
  background-color: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-3);
}
.m3e-confirm-panel--dialog {
  width: 100%;
  max-width: 28rem;
  border-radius: var(--md-sys-shape-corner-extra-large);
}
.m3e-confirm-panel--sheet {
  max-height: 90vh;
  max-height: 90dvh;
  border-radius: var(--md-sys-shape-corner-extra-large) var(--md-sys-shape-corner-extra-large) 0 0;
}
.m3e-confirm-panel__body {
  padding: var(--md-sys-spacing-6);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.m3e-confirm-panel--dialog .m3e-confirm-panel__body {
  max-height: calc(90dvh - 2 * var(--md-sys-spacing-6));
}
.m3e-confirm-panel--sheet .m3e-confirm-panel__body {
  max-height: 90vh;
  max-height: 90dvh;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--md-sys-spacing-6));
}
/* One left edge: the panel body pays the padding, so the title, copy and
   detail drop the dialog partial's own (A6a / D1-34). */
.m3e-confirm-panel .m3e-dialog__title { padding: 0; }
.m3e-confirm-panel__copy { margin-top: var(--md-sys-spacing-3); padding: 0; }
.m3e-confirm-panel__detail {
  margin: var(--md-sys-spacing-2) 0 0;
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-body-small-size);
  line-height: 1.5;
}
.m3e-confirm-word { margin-top: var(--md-sys-spacing-5); }
.m3e-confirm-word-label {
  display: block;
  margin-bottom: var(--md-sys-spacing-1);
  color: var(--md-sys-color-on-surface);
}
.m3e-confirm-word-input {
  width: 100%;
  height: 44px;
  padding: 0 var(--md-sys-spacing-3);
  border-radius: var(--md-sys-shape-corner-small);
  border: 1px solid var(--md-sys-color-outline);
  background: var(--md-sys-color-surface);
  color: var(--md-sys-color-on-surface);
  font: inherit;
}
.m3e-confirm-panel__actions { margin-top: var(--md-sys-spacing-6); padding: 0; }
.m3e-confirm-panel__actions--stacked { flex-direction: column; align-items: stretch; gap: var(--md-sys-spacing-2); }
.m3e-confirm-panel__actions--stacked > .m3e-button { width: 100%; }
.sheet-body-copy { text-align: left; line-height: 1.5; }
.sheet-body-copy p + p { margin-top: 0.5rem; }
.sheet-body-copy ul, .sheet-body-copy ol { margin-top: 0.5rem; padding-left: 1.1rem; }
.sheet-body-copy li + li { margin-top: 0.25rem; }

/* ── Mobile menu entrance / exit ── */
/* Enter 300ms; the panel is edge-anchored, so the standard spatial curve
   (no visible overshoot gap at the left edge). Exit is faster, on --ease-drop
   (motion spec §6); m3e-shell.js's menuExitDurationMs mirrors --duration-quick. */
.menu-entering #mobile-menu-backdrop { animation: menu-backdrop-in var(--duration-med) var(--easing-effects) forwards; }
.menu-entering #mobile-menu-panel { animation: menu-panel-in var(--duration-med) var(--md-sys-motion-easing-standard-spatial-default); }
.menu-exiting #mobile-menu-backdrop { animation: menu-backdrop-out var(--duration-quick) var(--ease-drop) forwards; }
.menu-exiting #mobile-menu-panel { animation: menu-panel-out var(--duration-quick) var(--ease-drop) forwards; }
/* Drawer swipe (m3e-shell.js): no transition while the finger drives; a
   dismissed swipe continues from its dragged offset on the same exit timing;
   a short swipe settles back. JS finishes on transitionend. */
#mobile-menu-panel.is-swiping { transition: none; }
#mobile-menu-panel.is-swipe-settling { transition: transform var(--duration-quick) var(--easing-effects); }
.menu-swipe-closing #mobile-menu-panel { animation: none; pointer-events: none; transition: transform var(--duration-quick) var(--ease-drop); }
.menu-swipe-closing #mobile-menu-backdrop { animation: menu-backdrop-out var(--duration-quick) var(--ease-drop) forwards; }

/* ── Nav item micro-interactions ── */
#mobile-nav-items > a {
  position: relative;
  transition:
    transform var(--md-sys-motion-duration-short2) var(--easing-effects),
    background var(--md-sys-motion-duration-short3) var(--easing-effects);
}
#mobile-nav-items > a:active { transform: scale(0.98); }
#mobile-nav-items > a[data-nav-active]::before {
  content: '';
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 3px; height: 20px;
  border-radius: var(--md-sys-shape-corner-extra-small);
  background: var(--md-sys-color-primary);
}

/* ── Tab bar transitions ── */
/* Crisp, not springy (review P2-9): these are tapped tens of times a day.
   No font-weight transition — weight cannot interpolate smoothly and reflows
   the label; it simply changes. */
[data-nav-id] svg {
  transition:
    color var(--md-sys-motion-duration-short4) var(--easing-effects),
    transform var(--md-sys-motion-duration-short4) var(--easing-effects);
}
[data-nav-id] span:last-child { transition: color var(--md-sys-motion-duration-short4) var(--easing-effects); }
.nav-active svg { transform: translateY(-1px); }
.nav-indicator { transition: opacity var(--md-sys-motion-duration-medium1) var(--easing-effects); }
[data-nav-id]:active { transform: scale(0.97); transition: transform var(--md-sys-motion-duration-short2) var(--easing-effects); }

/* The brand link and the skip link are real targets (44px), not 36px. */
.m3e-top-app-bar__brand { min-height: 44px; }
.skip-link {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}


/* ── Segmented control selected state ──

   Two layers, and only ever one of them on screen.

   1. `.is-seg-selected` — the selected BUTTON's own background. It is the
      truth: it needs no measurement, so it is right before JS runs, inside a
      menu that has never been opened, and if a measurement ever fails.
   2. `.seg-pill` — the sliding layer, which exists only to ANIMATE between
      states. It stays invisible until m3e-shell.js has taken a real
      measurement and marked the control [data-seg-ready]; from that point it
      is the visible one and layer 1 stands down.

   The owner's report was layer 2 shipping alone: the pill was measured one
   frame after the menu was displayed, while `.m3e-menu`'s enter animation
   still had the popup at scale(0.9), so it was locked ~10% narrow and short
   of the label's left edge. Layer 1 means a bad measurement can no longer
   produce a wrong-looking control, and the [data-seg-ready] gate means a
   missing one cannot either. */
[data-seg-control] .is-seg-selected {
  background: var(--md-sys-color-surface-container-highest);
  box-shadow: var(--md-sys-elevation-1);
}
[data-seg-control][data-seg-ready] .is-seg-selected {
  background: transparent;
  box-shadow: none;
}

.seg-pill {
  position: absolute;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container-highest);
  box-shadow: var(--md-sys-elevation-1);
  /* Animate only transform (a compositor property). Width is a layout property
     and is set instantly by JS — transitioning it thrashes layout. */
  transition: transform var(--md-sys-motion-duration-medium2) var(--ease-rally);
  top: 2px; bottom: 2px; left: 0;
  pointer-events: none;
  opacity: 0;
}
[data-seg-control][data-seg-ready] .seg-pill { opacity: 1; }

/* Compact: give segmented-control items a 44px touch height (visual via centering).
   Applies through the tablet breakpoint (839px) — these controls are touch
   targets on tablets too, not only phones. */
.m3e-seg-control__btn {
  /* Desktop density. Below 840px the block underneath raises it to 44px. */
  min-height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.m3e-seg-control__btn:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

@media (max-width: 839px) {
  [data-seg-control] [data-theme-btn],
  [data-seg-control] [data-contrast-btn],
  [data-seg-control] [data-portal-tab],
  .m3e-seg-control__btn,
  #mobile-portal-switcher [data-portal-tab] {
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}


/* L7-37: deleted. This painted a 2px blue underbar on `:focus` — i.e. on every
   mouse click — a SIXTH focus treatment, on top of the house ring, the text
   field wrapper's `:focus-within` border and `.m3e-select`'s ring. It was also
   clipped by any `overflow: hidden` ancestor, so it appeared on some fields and
   not others. The three treatments above already cover every field. */

/* ── Error message slide-in ── */
.field-error-animate {
  animation: error-slide-in var(--duration-quick) var(--easing-effects) forwards;
}

/* ── Read-only field visual distinction ── */
.field-readonly {
  cursor: not-allowed;
  transition: border-color var(--duration-quick) var(--easing-effects);
}
@media (hover: hover) and (pointer: fine) {
  .field-readonly:hover { border-color: var(--md-sys-color-primary-container); }
}

/* ── Tom Select, house-wide (r4 platform fix-up) ──────────────────────────
 * These rules used to live in three inline <style> blocks in
 * court_booking_settings.html and email_messaging_settings.html — banned by
 * house law, and page-scoped for a component that renders identically on every
 * surface. They are stated once, here, for every Tom Select in the product.
 * ───────────────────────────────────────────────────────────────────────── */

/* The control sits inside cards that clip; the dropdown has to escape. */
.ts-wrapper.single .ts-control { overflow: visible; }

/* ── The combobox panel (D5-05) ────────────────────────────────────────────
 * The one dropdown surface the app owns, and it was the one it had not
 * designed: `--surface-4` (the DEEPEST step of the ladder — the one that means
 * "recessed"), `border-radius: 0`, ~28px rows, options indented differently
 * from the control's own text. A flat grey slab pasted under the field, and
 * the only sub-44px interactive row left in the product.
 *
 * It is now the menu component: a light surface, the house radius, the house
 * shadow, 44px rows, the house hover wash, and an option indent equal to the
 * control's `padding-inline` so the text does not jump when the panel opens.
 * Dark mode inherits from the tokens — the four `[data-theme="dark"]` rules
 * that hard-restated all of this are deleted. */
.ts-dropdown {
  margin-top: var(--control-gap);
  padding-block: var(--md-sys-spacing-1);
  background: var(--surface-1);
  color: var(--fg1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
.ts-dropdown .option {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-inline: var(--control-pad-x);
  margin-inline: var(--md-sys-spacing-1);
  border-radius: var(--radius-xs);
  font-size: var(--control-fs);
  color: var(--fg1);
  background: transparent;
}
.ts-dropdown .option.active {
  background: color-mix(in srgb, var(--fg1) 8%, transparent);
  color: var(--fg1);
}
@media (hover: hover) and (pointer: fine) { .ts-dropdown .option:hover { background: color-mix(in srgb, var(--fg1) 8%, transparent); color: var(--fg1); } }
/* Keyboard-active is a different question from hover: it says "Enter picks
   this one", so it takes the brand wash, not the neutral one. */
.ts-dropdown .option.active { background: var(--brand-container); color: var(--brand-on-container); }
.ts-dropdown .option[aria-selected="true"],
.ts-dropdown .option.selected {
  font-weight: 500;
  background: var(--secondary-container);
  color: var(--secondary-on-container);
}
.ts-dropdown .optgroup-header {
  padding: var(--md-sys-spacing-2) var(--control-pad-x) var(--md-sys-spacing-1);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.4px;
  color: var(--fg2);
  background: transparent;
}
/* A4-57: the empty state of a combobox. `no_results` is rendered by
   initTomSelect; the copy comes from data-no-results on the <select>. */
.ts-dropdown .no-results {
  padding: var(--md-sys-spacing-3) var(--control-pad-x);
  color: var(--fg2);
  font-size: 13px;
  line-height: 1.5;
}
/* A native <select> popup is painted by the OS from the element's own colours;
   in dark mode an unstyled option is black-on-white inside a dark control. */
select option {
  color: var(--fg1);
  background: var(--surface-4);
}
select option:disabled {
  color: var(--fg2);
  background: var(--surface-2);
}
/* Inbound email bodies are third-party HTML; stop a wide image blowing out the
   page. (Was inline in email_messaging_settings.html.) */
.inbound-msg-body img { max-width: 100%; height: auto; }

/* D5-09: the six `[data-theme="dark"] .ts-*` overrides that used to sit here
   restated in raw tokens what the aliases already resolve — and one of them
   (`.ts-wrapper.focus { box-shadow: 0 0 0 2px primary-container }`) was a
   FOURTH focus treatment, visible in dark mode only. The aliases are
   theme-aware; there is nothing left for dark mode to say. */

/* ── The native <select> ──
   `appearance: none` so the box is ours; the chevron is drawn ONCE, as a mask
   on `.m3e-select-shell::after` / `.ts-wrapper.single::after` (see §S2 above),
   so the native select and the combobox are the same glyph. The gradient-
   diagonal chevron that used to live here — ~6.4px, square-cut ends, ~1.1px
   weight, unlike any other icon in the app — is gone, and so are the two
   dark-only `background-color` overrides: the fill is `--surface-0` in every
   theme now. */
select {
  appearance: none;
  -webkit-appearance: none;
  /* FALLBACK ONLY. A <select> cannot carry a pseudo-element, so the house
     chevron is drawn by its shell. A select that reaches the page without one
     — a Django widget rendered bare by a template that has not been routed
     through select.html yet — would otherwise have NO affordance at all, which
     is the D5-02 defect in a new shape. These two currentColor diagonals are
     the smaller, squarer chevron the mask replaces; they are here so the worst
     case is "not quite the house glyph" rather than "no glyph". The moment a
     select is wrapped, the rule below turns them off. */
  background-image:
    linear-gradient(45deg, transparent 44%, currentColor 46%, currentColor 54%, transparent 56%),
    linear-gradient(-45deg, transparent 44%, currentColor 46%, currentColor 54%, transparent 56%);
  background-repeat: no-repeat, no-repeat;
  background-position: right 17px center, right 12px center;
  background-size: 6px 6px, 6px 6px;
}
.m3e-select-shell > select { background-image: none; }

.ts-wrapper.single { position: relative; }
.ts-wrapper.single .ts-control {
  padding-right: calc(var(--control-pad-x) + var(--control-glyph)) !important;
}
.ts-wrapper.single.dropdown-active::after {
  transform: translateY(-50%) rotate(180deg);
}

/* ── Safe area helpers ── */
.pb-safe { padding-bottom: env(safe-area-inset-bottom, 0px); }

/* ── Court Grid Interactive Cells ─────────────────────────────────────── */
.m3e-court-grid-cell {
  cursor: pointer;
  transition: background-color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-court-grid-cell { position: relative; isolation: isolate; }
.m3e-court-grid-cell::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-court-grid-cell:hover::before        { opacity: 0.08; } }
.m3e-court-grid-cell:focus-visible::before { opacity: 0.12; }
.m3e-court-grid-cell:active::before       { opacity: 0.16; }
.m3e-court-grid-cell[aria-expanded="true"] {
  background-color: var(--md-sys-color-secondary-container);
}
/* Focus: the house inset ring at the end of this file. A 2px local outline
   loses to the house 3px anyway (L7 focus system, consequence 1). */
.m3e-court-grid-expand {
  border-top: 2px solid var(--md-sys-color-primary);
  background: var(--md-sys-color-surface-container-low);
}

/* ── Disclosure chevrons ─────────────────────────────────────────────────── */
/* One turn for every template chevron (review P2-9): --duration-quick on
   --ease-rally, no overshoot — the same as `.m3e-disclosure-trigger
   .toggle-chevron` and `.m3e-select-shell::after`. Tailwind's rotate-*
   utilities write the individual `rotate` property, so both are listed. */
.m3e-chevron {
  transition:
    transform var(--duration-quick) var(--ease-rally),
    rotate var(--duration-quick) var(--ease-rally);
}
details[open] > summary .details-chevron {
  transform: rotate(90deg);
}
/* Disclosures: the revealed content fades in instead of appearing in one
   frame. Opacity only, so no layout measurement inside a <details> changes. */
@supports selector(::details-content) {
  details[open]::details-content {
    animation: m3e-details-in var(--md-sys-motion-duration-short4)
               var(--md-sys-motion-easing-standard-effects-default);
  }
}
@keyframes m3e-details-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════════════
 * WAVE-2 PRIMITIVES — select, choice, metric, attention item, page header
 * Semantic aliases (--surface-0 …) resolve at use, so referencing them here
 * (before colors_and_type.css loads) is correct — see SKILL "Load order".
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Select (m3e/components/select.html) ─────────────────────────────────── */
/* Mode B renders <select class="m3e-select">; Mode A wraps a Django widget in
 * .m3e-select-field so the same rules style the browser-rendered <select>
 * (and reset _apply_house_widget_attrs() Tailwind), preserving its name/options/attrs. */
.m3e-select-field {
  display: flex;
  flex-direction: column;
  gap: var(--control-gap);
}
/* The box comes from the house control rule at the top of this file. Only the
   width and the room for the trailing glyph belong here. The `!important`
   chain this rule used to carry is gone: one rule owns the box, and a template
   that fights it is a template to fix, not a rule to shout over. */
.m3e-select,
.m3e-select-field select,
select {
  width: 100%;
  padding-right: calc(var(--control-pad-x) * 2 + var(--control-glyph));
}
.m3e-select-field.m3e-select-field--error select,
.m3e-select.m3e-select--error {
  border-color: var(--danger);
}

/* ── The chevron (D5-23 / §S2) ─────────────────────────────────────────────
 * `.m3e-select-shell` hugs the control and nothing else, so the glyph is
 * centred on the CONTROL rather than on a column that also holds a label and a
 * help line. `select.html` emits it; the template sweep wraps the raw selects
 * that remain. One mask, `currentColor`, so the native select and the Tom
 * Select combobox are pixel-identical in both themes and at every contrast
 * level — replacing the ~6.4px chevron the old rule built from two
 * `linear-gradient` diagonals with square-cut ends. */
.m3e-select-shell {
  position: relative;
  display: block;
  width: 100%;
}
.m3e-select-shell::after,
.ts-wrapper.single::after {
  content: '';
  position: absolute;
  right: var(--control-pad-x);
  top: 50%;
  transform: translateY(-50%);
  width: var(--control-glyph);
  height: var(--control-glyph);
  /* A token, not a baked hex inside a data-URI (which is what made the old
     chevron unable to follow `data-contrast`). Secondary ink: the glyph is an
     affordance, not content. */
  background-color: var(--fg2);
  -webkit-mask: var(--icon-chevron-down) center / contain no-repeat;
          mask: var(--icon-chevron-down) center / contain no-repeat;
  pointer-events: none;
  transition: transform var(--duration-quick) var(--ease-rally);
}
/* Tom Select replaces the <select> with its own wrapper INSIDE the shell, and
   the wrapper draws the same chevron. One is enough. */
.m3e-select-shell:has(.ts-wrapper)::after,
.m3e-select-shell:has(> select[multiple])::after,
.m3e-select-shell:has(> select[size]:not([size="1"]))::after { content: none; }
.m3e-select-shell > select,
.m3e-select-shell > .ts-wrapper { width: 100%; }
/* An unchosen select shows its placeholder in secondary ink, like every other
   control's placeholder. */
.m3e-select-shell:has(> select option[value=""]:checked) > select,
.m3e-select-field:has(select option[value=""]:checked) select { color: var(--fg2); }

/* ── Date / time / month (D5 §S3 — the native control, styled to the house) ─
 * NO custom picker. The native control gives locale formatting, keyboard
 * segment editing, mobile wheel pickers and screen-reader support that a
 * hand-built calendar would have to re-earn, and every date field in this
 * product is a single date with no range, no availability overlay and no
 * multi-month view. Build a house popover the day one of them grows a
 * two-month view — wrapping this same input, so the native path stays.
 *
 * The box comes from the house control rule. What is left is the glyph: the
 * UA paints its own filled calendar / clock in its own grey, which was the
 * third "there is more here" glyph in one screenshot. The picker indicator IS
 * a styleable pseudo-element, so it becomes the Heroicons mask in
 * `currentColor` — the UA glyph is replaced, the hit area is kept, and the
 * picker still opens from it. */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
  cursor: pointer;
  padding-right: var(--md-sys-spacing-2);
}
@media (hover: hover) and (pointer: fine) {
  input[type="date"]:hover,
  input[type="time"]:hover,
  input[type="datetime-local"]:hover,
  input[type="month"]:hover,
  input[type="week"]:hover { border-color: var(--brand); }
}
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="month"]::-webkit-calendar-picker-indicator,
input[type="week"]::-webkit-calendar-picker-indicator {
  width: var(--control-glyph);
  height: var(--control-glyph);
  margin: 0;
  padding: 0;
  opacity: 1;
  cursor: pointer;
  background-color: var(--fg2);
  background-image: none;
  -webkit-mask: var(--icon-calendar) center / contain no-repeat;
          mask: var(--icon-calendar) center / contain no-repeat;
}
input[type="time"]::-webkit-calendar-picker-indicator {
  -webkit-mask-image: var(--icon-clock);
          mask-image: var(--icon-clock);
}
input[type="date"]:disabled::-webkit-calendar-picker-indicator,
input[type="time"]:disabled::-webkit-calendar-picker-indicator,
input[type="datetime-local"]:disabled::-webkit-calendar-picker-indicator { display: none; }
/* The segment-by-segment keyboard editor highlighted in the OS `Highlight`
   blue — an un-themed colour inside a house field, in both themes. */
input[type="date"]::-webkit-datetime-edit,
input[type="time"]::-webkit-datetime-edit,
input[type="datetime-local"]::-webkit-datetime-edit,
input[type="month"]::-webkit-datetime-edit { font-variant-numeric: tabular-nums; }
::-webkit-datetime-edit-year-field:focus,
::-webkit-datetime-edit-month-field:focus,
::-webkit-datetime-edit-day-field:focus,
::-webkit-datetime-edit-hour-field:focus,
::-webkit-datetime-edit-minute-field:focus,
::-webkit-datetime-edit-ampm-field:focus {
  background-color: var(--brand-container);
  color: var(--brand-on-container);
  outline: none;
  border-radius: var(--radius-xs);
}
/* An empty date field prints `dd/mm/yyyy` at near-full ink weight, so a blank
   filter reads as though it holds a value. */
input[type="date"]:not(:focus):invalid::-webkit-datetime-edit,
input[type="time"]:not(:focus):invalid::-webkit-datetime-edit,
input[type="month"]:not(:focus):invalid::-webkit-datetime-edit { color: var(--fg2); }
/* A date field needs room for its own placeholder: `dd/mm/yyy` was measured
   with the last character cut off at 1440, and `dd/mm/y` at 1280. */
input[type="date"],
input[type="datetime-local"] { min-width: 190px; }
/* Inside a text-field wrapper the wrapper carries the surface; the control
   must not draw a second box inside it. */
.m3e-text-field__input-wrapper > input[type="date"],
.m3e-text-field__input-wrapper > input[type="time"],
.m3e-text-field__input-wrapper > input[type="datetime-local"],
.m3e-text-field__input-wrapper > input[type="month"],
.m3e-text-field__input-wrapper > input[type="week"] {
  min-width: 0;
  border: none;
  border-radius: 0;
  background-color: transparent;
}
/* A6a (D3-60's second half): `min-width: 0` above is needed so the control can
   shrink inside a flex wrapper, but it also threw away the 190px the date
   placeholder needs — which is why the picker glyph still sat on the last
   digits of `01/01/2026` in a two-column card AFTER every call site was routed
   through the house partial. The floor moves to the wrapper, where it is the
   box's own width rather than a flex item's basis. */
.m3e-text-field__input-wrapper:has(> input[type="date"]),
.m3e-text-field__input-wrapper:has(> input[type="datetime-local"]) {
  min-width: 190px;
}

/* ── Disclosure row (L7-24) ───────────────────────────────────────────────
 * `<summary>` shipped with no component of its own: 20-24px tall on phones
 * (under the touch floor on 100+ call sites), a native triangle in some places
 * and a hand-drawn chevron in others, and no hover anywhere while its
 * 8px-radius neighbours all had one. This is the house disclosure row. Native
 * semantics are untouched - it is still a <summary> inside a <details>. */
.m3e-disclosure__summary,
.m3e-card details > summary,
.m3e-disclosure > summary {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  min-height: 44px;
  /* A6a (B4 request 4). This used to be `padding-inline: 8px` with
     `margin-inline: -8px`, so the summary's border box was 16px WIDER than the
     <details> that contains it and every ancestor up to the card reported
     `scrollWidth > clientWidth` — 184 entries on the committee audit log alone,
     and lane B had to work around it per call site with `--inline w-full`. An
     8px cosmetic bleed is not worth a false horizontal-overflow reading on
     every page that carries a disclosure. The summary is flush with its own
     <details>; the `::before` state layer below covers exactly that box, which
     is the same contract `.m3e-card-row--interactive` already uses. */
  border-radius: var(--radius-sm);
  position: relative;
  isolation: isolate;
  cursor: pointer;
  list-style: none;
}
.m3e-disclosure__summary::-webkit-details-marker,
.m3e-card details > summary::-webkit-details-marker,
.m3e-disclosure > summary::-webkit-details-marker { display: none; }
.m3e-disclosure__summary::before,
.m3e-card details > summary::before,
.m3e-disclosure > summary::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-disclosure__summary:hover::before,
.m3e-card details > summary:hover::before,
.m3e-disclosure > summary:hover::before { opacity: 0.08; } }
.m3e-disclosure__summary:active::before,
.m3e-card details > summary:active::before,
.m3e-disclosure > summary:active::before { opacity: 0.16; }
/* The touch floor everywhere else a bare <summary> still lives. */
@media (max-width: 839px) {
  summary { min-height: 44px; display: flex; align-items: center; }
}

/* ── Choice row (m3e/components/choice.html) ─────────────────────────────── */
/* A full 44px-tall clickable row wrapping the existing m3e-checkbox/m3e-radio. */
.m3e-choice {
  display: flex;
  align-items: flex-start;
  gap: var(--md-sys-spacing-3);
  min-height: 44px;
  padding-block: var(--md-sys-spacing-2);
  cursor: pointer;
  /* L7-05: the app's canonical choice row had no hover, focus or pressed state
     at all. This used to give the wash 12px of side padding with a matching
     negative inline margin so the label stayed where the forms already put
     it — the same shape the disclosure summary had (A6a, B4 request 4): the
     row's border box ended up 24px WIDER than its <details>/form ancestor,
     so every container up to the card reported `scrollWidth > clientWidth`.
     The row is flush with its own ancestor instead; the `::before` state
     layer below covers exactly that box, same contract as
     `.m3e-card-row--interactive` and `.m3e-disclosure__summary`. */
  position: relative;
  isolation: isolate;
  border-radius: var(--radius-sm);
}
.m3e-choice::before {
  content: '';
  position: absolute;
  /* 2px of vertical inset, so two rows keep 8px between their washes even
     where the form's own container spacing is a 4px Tailwind `space-y-1`
     (which sits in @layer utilities and beats the sibling margin below). */
  inset: 2px 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-choice:hover::before               { opacity: 0.08; } }
.m3e-choice:has(:focus-visible)::before { opacity: 0.12; }
.m3e-choice:active::before              { opacity: 0.16; }
.m3e-choice:has(:disabled)::before      { opacity: 0 !important; }
/* ≥8px of unpainted ground between two neighbouring washes (§2 law 4). */
.m3e-choice + .m3e-choice { margin-top: var(--md-sys-spacing-2); }
.m3e-choice__control {
  display: flex;
  align-items: center;
  min-height: 24px;                               /* keeps the box optically centred on line 1 */
  flex: none;
}
.m3e-choice__text { display: flex; flex-direction: column; gap: var(--control-gap); }
/* One row label size, chosen by surface like every other control's ink:
   16px on member and public, 14px in committee/auditor work areas. Two
   checkbox partials used to disagree by 2px of type on the same page. */
.m3e-choice__label { font-size: var(--control-fs); color: var(--fg1); user-select: none; }
.m3e-choice__help { font-size: 12px; line-height: 1.4; color: var(--fg2); }
.m3e-choice__error { font-size: 12px; line-height: 1.4; color: var(--danger); }
.m3e-choice__label .m3e-required { color: var(--danger); }
/* A choice group's help belongs ABOVE the first option, under the legend —
   the same slot a field's help would occupy if the legend were a label. Under
   the second radio it reads as belonging to that option (D5-24). */
.m3e-choice-group__help {
  display: block;
  font-size: 12px;
  line-height: 1.4;
  color: var(--fg2);
  margin-bottom: var(--md-sys-spacing-2);
}

/* ── `.m3e-choice-card` — a radio option that is a TILE, not a row ──────────
 *
 * C2-10 / Decision 5. The two "How the Committee decides this" options on the
 * concession and promotion forms were written as
 * `.m3e-card.m3e-card--outlined.m3e-card--interactive` labels inside the
 * form's own outlined card — a card inside a card, and the ladder two levels
 * deep for what is one question. They are the same tile; they are just not
 * cards. Radius 12px (panel-in-card, Decision 5's radii-by-role), the card
 * hover contract (an 8% ink wash and nothing that moves), and the house focus
 * ring drawn on the tile because the radio inside it is 20px.
 *
 * Selection is `:has(:checked)` — the tile reads the control rather than the
 * template recomputing the server's answer in two Tailwind branches, so the
 * chosen tile is correct the instant the reader clicks it and not only after
 * the next render.
 *
 *   <label class="m3e-choice-card">
 *     <input type="radio" name="mode" value="…" class="m3e-radio__input">
 *     <span class="m3e-choice-card__text">…</span>
 *   </label>
 */
.m3e-choice-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--md-sys-spacing-3);
  padding: var(--md-sys-spacing-4);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--surface-0);
  cursor: pointer;
  transition:
    background-color var(--md-sys-motion-duration-short2)
      var(--md-sys-motion-easing-standard-effects-default),
    border-color var(--md-sys-motion-duration-short2)
      var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-choice-card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-choice-card:hover::before { opacity: 0.08; } }
.m3e-choice-card:active::before { opacity: 0.16; }
.m3e-choice-card:has(:focus-visible) {
  outline: 3px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
.m3e-choice-card:has(:checked) {
  border-color: var(--brand);
  background: var(--brand-container);
  color: var(--brand-on-container);
}
.m3e-choice-card > :not(input) { position: relative; min-width: 0; }
.m3e-choice-card__title { display: block; color: inherit; }
.m3e-choice-card__help { display: block; margin-top: 4px; color: var(--fg2); }
.m3e-choice-card:has(:checked) .m3e-choice-card__help { color: inherit; opacity: 0.85; }

/* ── Metric / KPI tile (core/_partials/_metric.html) ─────────────────────── */
/* Watch metric: quiet, informational, never alarm-coloured (decisions doc §3). */
.m3e-metric {
  display: block;
  padding: 14px 16px;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: inherit;
  text-decoration: none;
}
a.m3e-metric {
  transition:
    transform var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-spatial-default),
    background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-spatial-default);
}
a.m3e-metric { position: relative; isolation: isolate; }
/* Hover elevation fades by opacity on a shadow layer (review P2-6). */
a.m3e-metric::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: var(--shadow-1);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2) var(--easing-effects);
}
a.m3e-metric::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { a.m3e-metric:hover { transform: translateY(-1px); } a.m3e-metric:hover::after { opacity: 1; } }
@media (hover: hover) and (pointer: fine) { a.m3e-metric:hover::before        { opacity: 0.08; } }
a.m3e-metric:focus-visible::before { opacity: 0.12; }
a.m3e-metric:active::before       { opacity: 0.16; }
/* Focus: the house ring. A 2px local outline loses to the house 3px. */
.m3e-metric__value {
  margin-top: 6px;
  font-size: 24px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--fg1);
  font-variant-numeric: tabular-nums;             /* Inter tabular, never serif (OD-1) */
  font-feature-settings: "tnum";
}
.m3e-metric__value--text { font-size: 16px; font-weight: 600; }
.m3e-metric__sub { margin-top: 2px; font-size: 12px; color: var(--fg2); }
.m3e-metric__bar {
  margin-top: 10px;
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--surface-2);
  overflow: hidden;
}
.m3e-metric__bar-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);                       /* quiet good-standing teal, not alarm */
}

/* ── Attention queue row (core/_partials/_attention_item.html) ───────────── */
.m3e-attention-item {
  display: flex;
  align-items: center;
  gap: 12px;
  /* A6a (B2 request 1 / D2-11): the denser 56px recipe. A 32px count chip in
     12px block padding is exactly 56px, and the title and reason share ONE
     line when they fit (see __body below), so the row that measured 66-85px
     is 56px. Nine home rows now fit in ~500px and "Worth watching" sits level
     with the queue instead of leaving a 380x100px notch. */
  padding: 12px 16px;
  min-height: 56px;
  border-top: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
  transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-spatial-default);
}
.m3e-attention-item:first-child { border-top: none; }
/* L7-08: two hovered rows in sequence used to read as one block - a full-bleed
   opaque wash with square corners and 0px between neighbours. The hairline
   border-top is the list rule and stays; the wash is now inset and rounded. */
.m3e-attention-item { position: relative; isolation: isolate; }
.m3e-attention-item::before {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: var(--radius-sm);
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-attention-item:hover::before        { opacity: 0.08; } }
.m3e-attention-item:focus-visible::before { opacity: 0.12; }
.m3e-attention-item:active::before       { opacity: 0.16; }
/* Focus: the house inset ring at the end of this file. */
.m3e-attention-item__count {
  flex: none;
  min-width: 32px;
  height: 32px;
  padding: 0 6px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  /* A2 · D1 System 12: 15px is not a step on the m3e scale. */
  font-size: 16px;
  font-variant-numeric: tabular-nums;
}
/* warn tone uses the Wave-1 --warn role (court-line yellow wash) — never teal
 * (SKILL rule 2 / decisions OD-2). */
.m3e-attention-item__count--warn {
  background: var(--warn-container);
  color: var(--warn-on-container);
}
.m3e-attention-item__count--neutral { background: var(--surface-2); color: var(--fg1); }
/* Settled/handled rows (the Court plan "This month" rail's `done` items). Teal
 * is "all is well" and nothing else — SKILL rule 2 — which is exactly what a
 * done row is. */
.m3e-attention-item__count--done { background: var(--accent-container); color: var(--accent-on-container); }
/* Icon in the count slot: same 34px square, for rows that are a state rather
 * than a quantity. Keeps one row idiom across the three court hubs (D1-10). */
.m3e-attention-item__count--icon { font-size: 0; }
.m3e-attention-item__count--icon svg { width: 18px; height: 18px; }
/* B2 request 1: the body is ONE inline run. Title and reason sit on the same
   line separated by a middot when the row is wide enough, and the reason wraps
   under the title only when it genuinely does not fit — which is what the two
   stacked blocks were forcing on every row regardless of width. */
.m3e-attention-item__body { flex: 1; min-width: 0; }
.m3e-attention-item__title { font-size: 14px; font-weight: 600; color: var(--fg1); }
.m3e-attention-item__reason { font-size: 13px; color: var(--fg2); }
.m3e-attention-item__title + .m3e-attention-item__reason::before {
  content: ' · ';
  white-space: pre;
  color: var(--fg2);
}
.m3e-attention-item__meta {
  flex: none;
  font-size: 12px;
  color: var(--fg2);
  white-space: nowrap;
}
.m3e-attention-item__action { flex: none; font-size: 14px; font-weight: 600; color: var(--brand); }
/* The chevron variant (D2-20): a quiet 20px glyph that brightens on hover,
   in place of six identical "Open ->" links down a 320px rail. */
.m3e-attention-item__action--chevron {
  display: inline-flex;
  align-items: center;
  color: var(--fg2);
  transition: color var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-attention-item__action--chevron svg { width: 20px; height: 20px; }
@media (hover: hover) and (pointer: fine) { .m3e-attention-item:hover .m3e-attention-item__action--chevron { color: var(--brand); } }
.m3e-attention-item:focus-visible .m3e-attention-item__action--chevron { color: var(--brand); }
.m3e-attention-item__assigned { flex: none; }
@media (max-width: 839px) {
  .m3e-attention-item { flex-wrap: wrap; row-gap: 6px; padding: 12px 16px; }
  .m3e-attention-item__body { flex-basis: calc(100% - 44px); }
  .m3e-attention-item__assigned { margin-left: 44px; }
  .m3e-attention-item__action { margin-left: auto; }
}

/* ── Page header (core/_partials/_page_header.html) ──────────────────────────
 *
 * A2 · Decision 3 · D6-22 / D2-01 / D1 System 11.
 *
 * The identity band is the same shape and the same height on every page in the
 * product, and the H1 lands at ONE y. The arithmetic, from the top of the
 * viewport, is:
 *
 *   64  top app bar (--shell-top-bar-height)
 * + 12  #page-content's own pt-3
 * + 26  the reserved crumb row
 * +  6  gap
 * = 108 the H1's top edge, on every route, with or without a breadcrumb
 *
 * and the band's own height is
 *
 *   26 crumbs + 6 + 36 H1 row                            =  68px  (no subtitle)
 *   26 crumbs + 6 + 36 H1 row + 6 + 20 subtitle          =  94px  (one line)
 *
 * against the contract's ceilings of 96px and 128px. `margin-bottom` is the
 * gap to the page, not part of the band.
 * ─────────────────────────────────────────────────────────────────────────── */
.m3e-page-header { margin-bottom: 20px; }
/* A6a (B3 request 3 / the list-page contract). When the very next thing after
   the band is a card, the reader pays the band's 20px AND the card's own top
   padding — 44px of nothing between the lede and the first thing the page is
   about, on every register in the app. 12px here plus the card's padding is
   still a clear break. The card's own padding is a call-site utility, so this
   is the half of the gap the shared layer owns.

   Only when the card is the IMMEDIATE next sibling: a page whose band is
   followed by a tab strip, a filter row or a lede paragraph keeps the full
   20px, because there the band is being separated from something that is not
   already a box. */
.m3e-page-header:has(+ .m3e-card) { margin-bottom: 12px; }

/* The crumb slot. Height is reserved so an absent trail does not lift the H1;
   the pill inside it is `core/_partials/_breadcrumb.html`, the ONE breadcrumb
   component, whose own `mb-2` is a utility and therefore has to be overruled
   here rather than merely reset. */
.m3e-page-header__crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* 36px is the measured height of the breadcrumb pill at every width >= 840
     (the pill's own touch-sized links set it). The slot reserves that height
     whether the page has a trail, an overline, or neither, so the H1's top
     edge is the same number on every route: 64 (top bar) + 12 (#page-content's
     pt-3) + 36 + 6 = 118px. Change this and you move every H1 in the app. */
  min-height: 36px;
}
.m3e-page-header__crumbs > .m3e-breadcrumb { margin-bottom: 0 !important; }

.m3e-page-header__bar {
  display: flex;
  /* NOT `center`. The H1's box is 36px; centring it inside a bar made taller
     by a 44px control in the action slot pushes the title down by half the
     difference, and the H1's y is the one number this component exists to
     hold. The title's top edge is the bar's top edge, and anything taller
     hangs below it. */
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 6px;
  min-height: 36px;
}
/* Title + badge travel together and are what shrinks; the actions never do. */
.m3e-page-header__identity {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  flex: 1 1 auto;
  min-height: 36px;
}
.m3e-page-header__heading { min-width: 0; }
.m3e-page-header__actions {
  display: flex;
  align-items: center;
  min-height: 36px;
  gap: 8px;
  margin-left: auto;
  /* D2-51: a 190-character subtitle used to force the two header buttons into
     a vertical stack, because the subtitle shared the H1's flex row. The
     subtitle is now its own row, so the cluster is free to sit on one line at
     desktop widths without the lede pushing it apart.

     C2-02: it must still be ALLOWED to wrap. `flex: 0 0 auto` sizes this box
     at its own max-content and refuses to shrink, so a header carrying two
     actions ("New concession request" + "New promotion request", or /policy/'s
     two links) measured 433px inside a 386px phone and pushed the whole
     document sideways — four routes, 52 sweep rows, and Phase 1 had zero page
     overflow. `wrap` here costs nothing at 1280+ (max-content still fits on
     one line) and is the safety net everywhere else. */
  flex: 0 0 auto;
  flex-wrap: wrap;
  row-gap: var(--md-sys-spacing-2);
  justify-content: flex-end;
}
.m3e-page-header__overflow-btn { flex: none; }
/* A header action is a compact-density control, on every surface (D2-01: "the
   header action is 36px"). The band's ceiling is 96px without a lede and 128px
   with one; a 48px default button plus a two-line lede is 136px, and the page
   header is the one place in the product where the action's height is the
   band's height. Below 840px `.m3e-button--sm`'s own 44px floor still applies
   and the band wraps, so the touch target is never squeezed. */
.m3e-page-header__actions .m3e-button {
  min-height: var(--md-sys-density-touch-target-compact);
}
.m3e-page-header__kpi { text-align: right; flex: none; }
.m3e-page-header__kpi-value {
  font-size: 20px;
  font-weight: 700;
  color: var(--fg1);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* The lede. One sentence, 68ch, `--fg2`, and — by contract, enforced in copy
   review rather than here — no terminal full stop. */
.m3e-page-header__subtitle {
  margin-top: 6px;
  font-size: var(--md-sys-typescale-body-medium-size, 14px);
  line-height: 20px;
  color: var(--fg2);
  max-width: var(--measure-prose);
  text-wrap: pretty;
}
.m3e-page-header__meta {
  margin-top: 4px;
  font-size: 12px;
  line-height: 16px;
  color: var(--fg2);
  max-width: var(--measure-prose);
  font-variant-numeric: tabular-nums;
}

/* C2-02. Below the shell's own chrome boundary (840px, the same number the
   navigation rail and the 52px/16px control step use) the action cluster stops
   being a cluster and becomes a stack: it takes the whole line, and each button
   takes the whole line inside it. That is the pattern the member surface
   already ships (`w-full sm:w-auto`), and it is the only layout that is
   provably safe — a `.m3e-button` is `white-space: nowrap`, so any rule that
   lets two long labels share a 386px row either clips a word or scrolls the
   document. Buttons only; the "⋯" overflow trigger and any icon button stay
   their own size and sit at the end of the row. */
@media (max-width: 839px) {
  .m3e-page-header__actions {
    flex: 1 1 100%;
    margin-left: 0;
    justify-content: flex-start;
  }
  /* Every direct child takes the line — a button, or the `<div>`/`<form>` a
     page wrapped its buttons in (the members register and the court plan both
     do) — except the overflow menu, which is an icon and stays one. */
  .m3e-page-header__actions > *:not([data-m3e-menu-root]):not(.m3e-icon-button) {
    flex: 1 1 100%;
    min-width: 0;
  }
  /* Descendant, not child: reach the buttons inside those wrappers too.
     `flex-basis` rather than `width`, because a call site's `sm:w-auto` sits in
     `@layer utilities` and would beat a width declared here — a flex item's
     basis is what actually decides the line. */
  .m3e-page-header__actions .m3e-button:not(.m3e-icon-button) {
    flex: 1 1 100%;
  }
}

/* Below the chrome boundary the band is a phone header: the action cluster is
   allowed to fall under the title rather than squeeze it (D1-20). */
@media (max-width: 599px) {
  .m3e-page-header__identity { flex-basis: 100%; }
  /* The crumb row is reserved so the H1 lands at one y on a LAPTOP, which is
     where that contract is measured. On a phone the same 36px is a blank band
     under the app bar on every page that has no trail, and the phone's problem
     is the opposite one — D1-20: the member's sessions header eats 64% of the
     first screen. The row is only hidden when it is genuinely empty; the
     attribute is set by the component exactly then. */
  .m3e-page-header__crumbs[aria-hidden="true"] { display: none; }
}

/* The surface front door. No band geometry: an overline, the serif greeting,
   and whatever the page puts under it. */
.m3e-page-header--home { margin-bottom: 0; }
.m3e-page-header__hero { margin-top: 8px; font-size: clamp(28px, 6vw, 44px); }
.m3e-page-header--home .m3e-page-header__subtitle { margin-top: 10px; font-size: 16px; line-height: 24px; }


/* ═══════════════════════════════════════════════════════════════════════════
 * ERROR PAGE (core/_partials/_error_page.html)
 * One presentation for 403 / 404 / 500 and every in-app error response.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-error-page {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--md-sys-spacing-8) var(--md-sys-spacing-4);
}
.m3e-error-page--standalone {
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--md-sys-color-surface-container-low);
}
.m3e-error-page__panel {
  width: 100%;
  max-width: 30rem;
  padding: var(--md-sys-spacing-8);
  text-align: center;
  /* L1-17: the panel is a card, and SKILL rule 6 says shadows are for dialogs,
     bottom sheets, FABs and the sticky top bar on scroll — never on cards. The
     surface ladder plus the 1px outline carries the separation. */
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--md-sys-shape-corner-large);
}
.m3e-error-page__mark {
  width: 48px;
  height: 48px;
  margin: 0 auto var(--md-sys-spacing-6);
  display: flex;
}

.m3e-error-page__art {
  width: 120px;
  height: 96px;
  margin: 0 auto var(--md-sys-spacing-6);
  display: block;
}
/* `.m3e-error-page__code` deleted: `_error_page.html` stopped emitting it when
   L1-16 made the status code a footnote rather than the headline (Wave B1).
   `.m3e-error-page__reference` below now carries it — "Error 403", or
   "Error 500 · Reference ABC123" — and its quiet treatment is the right one. */
.m3e-error-page__title {
  font-family: var(--md-sys-typescale-headline-small-font, inherit);
  font-size: 24px;
  line-height: 32px;
  font-weight: 600;
  color: var(--md-sys-color-on-surface);
  margin: var(--md-sys-spacing-2) 0 0;
}
.m3e-error-page__message {
  font-size: 16px;
  line-height: 24px;
  color: var(--md-sys-color-on-surface-variant);
  margin: var(--md-sys-spacing-3) 0 0;
}
.m3e-error-page__actions {
  display: flex;
  flex-direction: column;
  gap: var(--md-sys-spacing-3);
  margin-top: var(--md-sys-spacing-8);
}
@media (min-width: 600px) {
  .m3e-error-page__actions {
    flex-direction: row;
    justify-content: center;
  }
}
.m3e-error-page__reference {
  font-size: 12px;
  color: var(--md-sys-color-on-surface-variant);
  margin: var(--md-sys-spacing-6) 0 0;
  user-select: all;
}



/* ═══════════════════════════════════════════════════════════════════════════
 * FORM VALIDATION SUMMARY (form-validation.js)
 * One alert at the top of the form listing every error as a link to its
 * control. Errors used to be discoverable only one at a time (A1-05).
 * ═══════════════════════════════════════════════════════════════════════════ */

.validation-summary {
  margin-bottom: var(--md-sys-spacing-4);
  padding: var(--md-sys-spacing-4);
  border-radius: var(--md-sys-shape-corner-medium);
  background: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container);
}
.validation-summary:focus { outline: none; }
.validation-summary__title {
  margin: 0;
  /* A2 · D1 System 12: 15px is not a step on the m3e scale. */
  font-size: 16px;
  font-weight: 600;
}
.validation-summary__list {
  margin: var(--md-sys-spacing-2) 0 0;
  padding-left: var(--md-sys-spacing-5);
  font-size: 14px;
  line-height: 22px;
}
.validation-summary__list a {
  color: inherit;
  text-decoration: underline;
}
.validation-summary__list a:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}
.validation-error {
  animation: error-slide-in var(--duration-quick) var(--easing-effects) both;
  color: var(--md-sys-color-error);
}
.validation-success-icon { color: var(--md-sys-color-tertiary); }

/* ── Client-side field state (A2 platform note) ───────────────────────────
 * The ONE way a page module marks a control invalid. Before r4 every module
 * wrote `field.style.borderColor` itself — an inline style no theme, no
 * high-contrast mode and no stylesheet could reach, in a slightly different
 * red each time. `form-validation.js` sets these classes; nothing writes
 * `style.borderColor` any more. Server-rendered errors use the partial's own
 * `--error` state and do not need these.
 * ───────────────────────────────────────────────────────────────────────── */
.npsc-field-invalid,
.npsc-field-invalid:hover {
  border-color: var(--md-sys-color-error) !important;
}
.npsc-field-valid,
.npsc-field-valid:hover {
  border-color: var(--md-sys-color-tertiary) !important;
}
/* Inside a filled M3E text field the input has no border of its own — the
   wrapper draws it, so the state has to travel one level up. */
.m3e-text-field__input-wrapper:has(> .npsc-field-invalid) {
  border-color: var(--md-sys-color-error) !important;
}
.m3e-text-field__input-wrapper:has(> .npsc-field-valid) {
  border-color: var(--md-sys-color-tertiary) !important;
}



/* ═══════════════════════════════════════════════════════════════════════════
 * SIGNATURE CAPTURE (core/_partials/_signature_capture.html)
 *
 * The pad is fixed white paper with fixed #191C20 ink in every theme — the
 * exported PNG has to read on any background, so theming stops at the chrome.
 * That is the one documented colour exception in the product.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-signature-pad {
  position: relative;
  overflow: hidden;
  touch-action: none;
  border-radius: var(--md-sys-shape-corner-medium);
  border: 2px solid var(--md-sys-color-outline-variant);
  background: #ffffff;
}
.m3e-signature-pad__canvas {
  display: block;
  width: 100%;
  height: 200px;
}
/* The baseline. A signature needs a line to sit on; without one people sign in
   the middle of the box and the exported image is off-centre. */
.m3e-signature-pad__baseline {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 48px;
  height: 1px;
  background: rgb(25 28 32 / 0.28);
  pointer-events: none;
}
/* R3-07: this pseudo-element used to render the multiplication-sign glyph as
   real CSS text content, which some assistive-tech/browser combinations
   exposed as a bare, unlabelled text node in the accessibility tree despite
   the parent baseline element already carrying `aria-hidden="true"` (reported
   near the signature-method toggle on both the governance minutes sign-off
   and the auditor sign-off pages, which share this widget). It is purely
   decorative — draw it without any text content so nothing is left to leak
   into the tree. */
.m3e-signature-pad__baseline::before {
  content: '';
  position: absolute;
  left: -14px;
  bottom: -6px;
  width: 12px;
  height: 12px;
  background:
    linear-gradient(45deg, transparent 45%, rgb(25 28 32 / 0.35) 45%, rgb(25 28 32 / 0.35) 55%, transparent 55%),
    linear-gradient(-45deg, transparent 45%, rgb(25 28 32 / 0.35) 45%, rgb(25 28 32 / 0.35) 55%, transparent 55%);
}
.m3e-signature-pad__placeholder {
  position: absolute;
  left: 8%;
  bottom: 52px;
  pointer-events: none;
  color: rgb(25 28 32 / 0.35);
}
/* The Type-mode name field. The shared signature partial used to style this
   inline with `outline-none focus:ring-2` plus a `--tw-ring-color` custom
   property, which is the one focus treatment the house forbids duplicating
   (Q7-42). One class, the house ring, no inline style. */
/* D5-01: this was a seventh control family — 48px, `--radius-md`, an
   `--outline-variant` edge and its own 2px focus outline. It is the house
   control box now; only the display and width belong to it. */
.m3e-signature-typed-input {
  display: block;
  width: 100%;
  min-height: var(--control-h-form);
  padding-inline: var(--control-pad-x);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--control-fill);
  color: var(--control-ink);
  font-family: inherit;
  font-size: var(--control-fs);
  transition: border-color var(--duration-quick) var(--ease-rally);
}
@media (hover: hover) and (pointer: fine) {
  .m3e-signature-typed-input:hover { border-color: var(--brand); }
}
.m3e-signature-typed-input:focus-visible { border-color: var(--brand); }

/* Q7-41 / Q8-31: the typed preview is a sheet of paper, and in dark mode a
   bare white slab with no frame reads as a rendering fault. Give it the pad's
   chrome — border, radius and a baseline — so it is legible as paper. */
.m3e-signature-typed-preview {
  position: relative;
  font-family: 'Alex Brush', cursive;
  font-size: 2.5rem;
  line-height: 1.2;
  min-height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--md-sys-spacing-4) var(--md-sys-spacing-4) calc(var(--md-sys-spacing-4) + 10px);
  text-align: center;
  overflow: hidden;
  border-radius: var(--md-sys-shape-corner-medium);
  border: 2px solid var(--md-sys-color-outline-variant);
  /* Same fixed paper/ink as the pad: this previews the exported artifact. */
  background: #ffffff;
  color: #191C20;
}
.m3e-signature-typed-preview::after {
  content: '';
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 18px;
  height: 1px;
  background: rgb(25 28 32 / 0.28);
  pointer-events: none;
}
.m3e-signature-preview {
  max-height: 6rem;
  border-radius: var(--md-sys-shape-corner-small);
  border: 1px solid var(--md-sys-color-outline-variant);
  background: #ffffff;
}



/* Password reveal control — 44x44 target over a 20px glyph. */
.m3e-password-reveal {
  position: absolute;
  right: calc(var(--md-sys-spacing-4) - 12px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: var(--md-sys-shape-corner-full);
}
/* [hidden] is a UA rule that also applies to SVG, but M3E sets display on many
   elements; make the intent explicit for the reveal glyphs. */
.m3e-password-reveal svg[hidden] { display: none; }
.m3e-password-reveal:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}



/* ═══════════════════════════════════════════════════════════════════════════
 * PAGINATION (core/_partials/_pagination.html)
 * ═══════════════════════════════════════════════════════════════════════════ */

/* One landmark per pager (Q4-38). The region wraps the range line, the desktop
   row and the phone row, so a page with a top and a bottom pager offers two
   named landmarks rather than four identical ones. */
.m3e-pagination-region { display: block; }

.m3e-pagination__range {
  margin: 0 0 var(--md-sys-spacing-3);
  font-size: 13px;
  color: var(--md-sys-color-on-surface-variant);
}
/* Every pager control is a real target. The page-number links were 34x25px,
   which is a coin-flip on a phone. */
.m3e-pagination a,
.m3e-pagination > span,
.m3e-pagination [aria-current="page"] {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.m3e-pagination a {
  min-width: 44px;
}
.m3e-pagination a:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
@media (min-width: 840px) {
  /* Desktop density: a pointer does not need 44px, and a row of 44px page
     numbers reads as a toolbar rather than a pager. */
  .m3e-pagination a,
  .m3e-pagination > span,
  .m3e-pagination [aria-current="page"] {
    min-height: 36px;
  }
  .m3e-pagination a { min-width: 36px; }
}



/* ═══════════════════════════════════════════════════════════════════════════
 * STAT TILE (core/_partials/_stat_tile.html)
 *
 * The tone used to be sixteen inline `style=` declarations per meeting page —
 * a background and an ink colour repeated on the container, the label, the
 * value and the description. One custom property does the whole job: the tone
 * modifier sets --stat-tile-ink, every line inside reads it, and the tile has
 * a single place where its colour decision lives.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* A3 / Decision 5 — the tile is a PANEL inside a card, not a second card. It
   used to carry `m3e-card m3e-card--filled`, which made every KPI row in the
   app a nest (requisitions, the ballot analytics trio, the finance hub). Its
   own surface and its own radius now live here: the inert `--surface-4` fill is
   correct for a tile (a figure is inert content), and 12px is the panel-in-card
   step of the radius-by-role table. `position: relative` replaces what
   `.m3e-card` used to supply for the interactive variant's state layer. */
.m3e-stat-tile {
  padding: 20px;
  position: relative;
  background-color: var(--md-sys-color-surface-container-highest);
  border-radius: var(--md-sys-shape-corner-medium);
  overflow: clip;
  overflow-clip-margin: 6px;
  --stat-tile-ink: var(--fg2);
  --stat-tile-value-ink: var(--fg1);
}
.m3e-stat-tile__label { margin: 0; color: var(--stat-tile-ink); }
.m3e-stat-tile__caption {
  margin: 6px 0 0;
  font-weight: 600;
  color: var(--stat-tile-value-ink);
}
.m3e-stat-tile__value { margin: 4px 0 0; color: var(--stat-tile-value-ink); }
.m3e-stat-tile__description { margin: 6px 0 0; color: var(--stat-tile-ink); }

.m3e-stat-tile--urgent {
  background: var(--danger-container);
  --stat-tile-ink: var(--danger-on-container);
  --stat-tile-value-ink: var(--danger-on-container);
}
.m3e-stat-tile--warning {
  background: var(--warning-container);
  --stat-tile-ink: var(--warning-on-container);
  --stat-tile-value-ink: var(--warning-on-container);
}


/* ═══════════════════════════════════════════════════════════════════════════
 * BREADCRUMB (core/_partials/_breadcrumb.html)
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-breadcrumb { max-width: 100%; }
.m3e-breadcrumb__current { color: var(--fg1); font-weight: 500; }
.m3e-breadcrumb__link { display: inline-flex; align-items: center; }
/* L6-45: a chevron pointing at empty space. When the current page's name wraps,
   line 1 used to end "General meetings ›" with nothing after it. The separator
   and the segment it introduces are one unbreakable unit. */
.m3e-breadcrumb__segment {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  white-space: nowrap;
  min-width: 0;
}
.m3e-breadcrumb__segment > .m3e-breadcrumb__crumb { min-width: 0; }

/* A crumb link was a 17px-tall word — the smallest target in the product, and
   the one people reach for when they want to go back. Below 840px the LINK
   grows to 44px; the pill keeps its compact look because the extra height is
   negative-margined back out of the row rather than added to it. */
@media (max-width: 839px) {
  .m3e-breadcrumb {
    padding-top: 0;
    padding-bottom: 0;
    flex-wrap: wrap;
  }
  .m3e-breadcrumb__link,
  .m3e-breadcrumb__current {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    /* A 44px-tall target 36px wide is still a miss (Q8-19). Pad it out and
       negative-margin the padding back so the row reads unchanged. */
    min-width: 44px;
    padding-inline: 6px;
    margin-inline: -6px;
  }
}
/* L5-22: three crumbs plus two chevrons wrap the pill into a two-line blob at
   386px. Below `sm:` only the LAST parent and the current page are shown — the
   one step back is the one a phone reader actually uses, and the full trail
   stays in the DOM for assistive tech and for wider screens. */
@media (max-width: 639px) {
  /* The SEGMENT is the unit (chevron + crumb, L6-45), so it is the segment
     that is hidden — hiding the crumb alone left a pill containing nothing but
     chevrons. */
  .m3e-breadcrumb__segment:not(.m3e-breadcrumb__crumb--last-parent):not(.m3e-breadcrumb__crumb--current) {
    display: none;
  }
  /* With the earlier segments gone, the first one still standing must not lead
     with a chevron pointing at nothing behind it. */
  .m3e-breadcrumb__crumb--last-parent > .m3e-breadcrumb__sep { display: none; }
}
/* F-02: the segment is `white-space: nowrap` so a chevron never dangles at the
   end of a line — right for a parent crumb, wrong for the current page's own
   name, which is as long as whatever the page is called. "Extraordinary
   General Meeting - Bylaw Amendment (QA Sample)" measured 335px in a 270px
   pill on a cover screen and simply ran off the end: no ellipsis, no tooltip,
   71px of the title unreadable. The current crumb wraps, at every width — the
   860px Fold, which takes the desktop drawer on a small screen, breaks it just
   as a phone does, and a long page name overruns the pill on a laptop too.
   Parent crumbs keep their nowrap: they are short by construction, and the
   pill itself wraps so a whole segment moves to the next line instead. */
.m3e-breadcrumb { flex-wrap: wrap; }
.m3e-breadcrumb__crumb--current,
.m3e-breadcrumb__current {
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: start;
  justify-content: flex-start;
}
@media (min-width: 840px) {
  .m3e-breadcrumb { padding-block: 0.375rem; }
  /* L4-10 (B4): a 36x16 crumb on a laptop. The 44px floor above is the touch
     rule and turning the pill into a 56px button bar on a desktop would be the
     wrong trade — but 16px tall fails WCAG 2.5.8 (24x24) even for a mouse, and
     16px was never a comfortable click. 24px tall, 44px wide, with the extra
     negative-margined back out so the pill's own height is unchanged. */
  .m3e-breadcrumb__link,
  .m3e-breadcrumb__current {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 24px;
    min-width: 44px;
    padding-inline: 6px;
    margin-inline: -6px;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
 * SEGMENTED CONTROL (core/_partials/_segmented.html)
 *
 * Selected state is painted from aria-checked, which is the state a screen
 * reader reads. Before r4 the selected look was six inline style properties
 * written by an inline <script>, so the two could disagree and no theme could
 * reach either.
 * ═══════════════════════════════════════════════════════════════════════════ */

.m3e-segmented {
  display: flex;
  padding: 2px;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container);
}
.m3e-segmented__btn {
  flex: 1;
  min-height: var(--md-sys-density-touch-target-compact);
  padding: 0 var(--md-sys-spacing-3);
  border: none;
  border-radius: var(--md-sys-shape-corner-small);
  background: transparent;
  color: var(--md-sys-color-on-surface-variant);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default),
    color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
.m3e-segmented__btn[aria-checked="true"],
.m3e-segmented__btn[aria-selected="true"] {
  background: var(--md-sys-color-surface);
  color: var(--md-sys-color-on-surface);
  box-shadow: var(--md-sys-elevation-1);
}
/* The state layer (L7-11): a segmented control had no hover at all. */
.m3e-segmented__btn { position: relative; isolation: isolate; }
.m3e-segmented__btn::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2)
              var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-segmented__btn:hover::before        { opacity: 0.08; } }
.m3e-segmented__btn:focus-visible::before { opacity: 0.12; }
.m3e-segmented__btn:active::before       { opacity: 0.16; }

/* L7-03: the pill variant — the ONE sanctioned exception to the tab strip.
   The login switch is a two-way sign-in METHOD switch, not navigation, so it
   keeps its full-radius shape; it stops being page-local Tailwind and becomes
   this component. */
.m3e-segmented--pill,
.m3e-segmented--pill .m3e-segmented__btn {
  border-radius: var(--md-sys-shape-corner-full);
}
.m3e-segmented--pill .m3e-segmented__btn {
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  font-size: 14px;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * THE HOUSE FOCUS RING — one treatment, every control (A1-51/52/53, A7-28)
 *
 * This is the LAST block in the file on purpose: it is the floor, and it must
 * beat every component rule above it as well as Tailwind's `focus:outline-none`
 * (which sits in `@layer utilities`, above `@layer m3e`). `!important` inside a
 * layer reverses layer order, which is why it is used here and why it must not
 * be used anywhere else for focus.
 *
 * Tokens live in colors_and_type.css. If you are reaching for a per-component
 * focus rule, you are about to create the sixth focus treatment — don't.
 * The only sanctioned variation is the INSET ring, listed separately below,
 * for controls flush with a clipping edge.
 * ═══════════════════════════════════════════════════════════════════════════ */

:where(
  a[href],
  button,
  input,
  select,
  textarea,
  summary,
  [tabindex]:not([tabindex="-1"]),
  [role="button"],
  [role="tab"],
  [role="switch"],
  [role="checkbox"],
  [role="radio"],
  [role="menuitem"],
  [role="option"],
  [role="link"]
):focus-visible {
  outline: var(--m3e-focus-ring-width) solid var(--m3e-focus-ring-color) !important;
  outline-offset: var(--m3e-focus-ring-offset) !important;
}

/* Text fields: the ring goes on the WRAPPER, because the bare <input> inside a
   filled M3E text field has no border, no radius and full-bleed padding — a
   ring on it draws a rectangle through the label. The input itself keeps no
   outline; `:has()` is supported everywhere this product runs, and the
   `:focus-within` rule is the fallback for the same shape. */
.m3e-text-field__input-wrapper > input:focus-visible,
.m3e-text-field__input-wrapper > textarea:focus-visible,
.m3e-text-field__input-wrapper > select:focus-visible {
  outline: none !important;
  outline-offset: 0 !important;
}
.m3e-text-field__input-wrapper:has(> input:focus-visible),
.m3e-text-field__input-wrapper:has(> textarea:focus-visible),
.m3e-text-field__input-wrapper:has(> select:focus-visible) {
  outline: var(--m3e-focus-ring-width) solid var(--m3e-focus-ring-color) !important;
  outline-offset: var(--m3e-focus-ring-offset) !important;
}

/* L3-08: Tom Select renders its own `.ts-control` in place of the real
   `<select>`, which is `display: none` — so the house block above matches an
   element nobody can see, and the app's most common committee filter was the
   one control a keyboard user could not locate. The ring goes on the visible
   proxy, the same 3px at the same offset as everything else. */
.ts-wrapper.focus,
.ts-wrapper.input-active {
  outline: var(--m3e-focus-ring-width) solid var(--m3e-focus-ring-color) !important;
  outline-offset: var(--m3e-focus-ring-offset) !important;
}
.ts-wrapper.focus > .ts-control,
.ts-wrapper.input-active > .ts-control {
  outline: none !important;
}

/* Inset ring — flush controls, where an outset ring would be clipped by the
   scroll container (drawer rows, menu items, tabs, table action cells). */
.m3e-nav-drawer__item:focus-visible,
.m3e-menu__item:focus-visible,
.m3e-tabs__tab:focus-visible,
/* L7-15: every chip rail in the app is an `overflow-x: auto` container, which
   clips on BOTH axes, so an outset ring was sheared off top and bottom. */
.m3e-chip:focus-visible,
/* L7-17: page-local tab strips live inside `m3e-card overflow-hidden p-1`,
   which leaves the outset ring exactly 5px — and the card's own 1px border
   sits inside that. One line fixes in-component tabs, the five page-local tab
   bars, and anything else that ever carries the role. */
[role="tab"]:focus-visible,
/* L7-16: this drew an inset ring AND the house outset ring at the same time. */
.m3e-list__item[href]:focus-visible,
.m3e-court-grid-cell:focus-visible,
.m3e-attention-item:focus-visible,
.m3e-account-block:focus-visible,
.m3e-account-menu__appearance-summary:focus-visible,
/* The drawer quick-find sits inside the drawer body's own scroll container. */
.m3e-nav-drawer__quick-find:focus-visible,
/* Table action cells — named in the block comment above as one of the reasons
   the inset variation exists. `.m3e-table-wrapper` is `overflow-x: auto`, which
   computes to `auto` on BOTH axes, so the "Open" button in the last column of
   every register had the top and bottom of its ring sheared off (L7-15). */
.m3e-table-wrapper .m3e-button:focus-visible,
.m3e-table-wrapper .m3e-chip:focus-visible,
.m3e-card details > summary:focus-visible,
.m3e-disclosure > summary:focus-visible,
.m3e-disclosure__summary:focus-visible {
  outline: none !important;
  box-shadow: var(--m3e-focus-ring-inset) !important;
}

/* On an inverse surface the primary ring disappears. Two exceptions, both of
   them a colour swap only — the geometry is unchanged. */
.m3e-snackbar :focus-visible,
.bg-slate-900 a:focus-visible,
.bg-slate-950 a:focus-visible {
  --m3e-focus-ring-color: var(--md-sys-color-inverse-primary);
}

/* A scripted focus (the heading after a boosted swap, a validation summary)
   is not a user focus and must not paint. These carry tabindex="-1", so
   :focus-visible never matches them anyway; this kills the UA :focus outline. */
:where(h1, h2, h3, [role="region"], .validation-summary)[tabindex="-1"]:focus {
  outline: none !important;
  box-shadow: none !important;
}


/* ══════════════════════════════════════════════════════════════════════════
 * FORM ACTION BAR  (Phase 2 A4 · D6-21 / D6 System 8)
 *
 * D6 measured eleven pages whose only filled button sits below the fold — one
 * of them 8.3 folds of toggles with Save at the very end, so a reader who
 * changed the first toggle had 6,000 px to scroll to commit it.
 *
 * `position: sticky` is the whole mechanism, and it is why there is no JS: a
 * footer that fits on screen stays exactly where it was written, and one whose
 * form is taller than the viewport pins itself to the bottom edge until the
 * end of the form scrolls into view. "Always visible when the form is taller
 * than the viewport, hidden otherwise" is what sticky already means.
 *
 * It sits on `--z-page-bar`, the rung colors_and_type.css reserves for a
 * page's own action bar (above the top app bar and the phone tab bar, below
 * every scrim), and clears the phone tab bar with the same
 * `--m3e-bottom-nav-height` token the rest of the shell uses.
 *
 * Usage: put the class on the row that already holds the form's submit —
 *   <div class="m3e-form-actions">…the buttons…</div>
 * `--between` / `--start` change only where the buttons sit in the row.
 *
 * ONE placement rule, and it is the one that catches people: a sticky element
 * is bounded by its own containing block, and `.m3e-card` is `overflow: clip`,
 * which makes the card that element's scrollport. So the bar must be a child
 * of the TALL thing — the long form, or the page column — not of a short card
 * at the end of it. Measured at 1280x800: inside the 100px trailing card on
 * /committee/system/communications/ the bar never pinned; as a child of the
 * column it pins from the first screen. Verify with the bar's
 * `getBoundingClientRect().bottom` at scroll 0: it should equal the viewport
 * height on a page taller than the viewport.
 * ══════════════════════════════════════════════════════════════════════════ */

.m3e-form-actions {
  position: sticky;
  bottom: 0;
  z-index: var(--z-page-bar);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--md-sys-spacing-2);
  margin-block-start: var(--md-sys-spacing-5);
  padding-block: var(--md-sys-spacing-3);
  /* No negative side margins: the bar keeps its container's own padding, so
     the recipe is the same whether the form sits in a p-5 card, a p-6 card or
     no card at all. Nothing shows through the gutters — the content it covers
     is inset by that same padding. */
  background: var(--surface-0);
  border-top: 1px solid var(--border);
}

/* A form whose actions belong on the left (a wizard's Back/Next pair) or which
   wants them spread. Layout only; the pinning is unchanged. */
.m3e-form-actions--between { justify-content: space-between; }
.m3e-form-actions--start { justify-content: flex-start; }

/* The bar is the LAST thing on the page on a phone, where the tab bar owns the
   bottom edge; lift it clear rather than letting the two overlap. */
@media (max-width: 839px) {
  .m3e-form-actions {
    bottom: calc(var(--m3e-bottom-nav-height) + env(safe-area-inset-bottom, 0px));
  }
}

/* Printing a settings page should print the settings, not a pinned bar. */
@media print {
  .m3e-form-actions {
    position: static;
    border-top: 0;
  }
}

/* A5 residual: the bar pins to the bottom of its own containing block once
   that block (the tall column, or — as on a single-card page like the
   expense form — the card itself) is even slightly taller than the
   viewport. Two knock-on effects follow, and both are sized to one row of
   the bar (its padding + a single --control-h-form button + the margin that
   separates it from the field above), which is generous enough to also
   cover a bar that wraps to two rows:
     1. a keyboard tab, an anchor jump, or the validation summary's per-field
        links (D5-07) can land the target field exactly behind the pinned
        bar. `scroll-padding-bottom` on the scrolling root keeps the browser's
        own scroll-into-view clear of it.
     2. the form's own last field needs the same clearance once a reader
        scrolls all the way, so a short form doesn't end with its final
        control tucked under the bar. */
form:has(.m3e-form-actions) {
  --m3e-form-actions-clearance: calc(
    var(--control-h-form) + (var(--md-sys-spacing-3) * 2) + var(--md-sys-spacing-5) + 1px
  );
  padding-bottom: var(--m3e-form-actions-clearance);
  scroll-padding-bottom: var(--m3e-form-actions-clearance);
}
html:has(.m3e-form-actions) {
  scroll-padding-bottom: calc(
    var(--control-h-form) + (var(--md-sys-spacing-3) * 2) + var(--md-sys-spacing-5) + 1px
  );
}


/* QA 2026-09-25 (Kenny: "All these small things are important."). Headings
   and short labels balance their lines so a two-word heading never leaves one
   word alone ("Coming" / "up"); body copy uses `pretty`, which stops a single
   short word from ending a paragraph ("…the committee's resolution" left
   "day." or "hours" alone). Dates, times and amounts are already unbreakable:
   the house filters join them with no-break spaces. */
h1, h2, h3, h4, h5, h6, dt, th, label, legend, .m3e-chip__label {
  text-wrap: balance;
}
p, li, dd, td, figcaption, .m3e-card-row__supporting {
  text-wrap: pretty;
}

/* QA 2026-09-25 · sessions */
/* The court bookings attention row: the reason is its own muted line under the
   title rather than an inline " · " continuation, which wrapped to start a
   line with the dot on a phone. Scoped to this call site's modifier. */
.npsc-attention-item--stacked .m3e-attention-item__reason { display: block; }
.npsc-attention-item--stacked .m3e-attention-item__title + .m3e-attention-item__reason::before {
  content: none;
}

/* QA 2026-09-25 · auditor */
/* One financial-year control on the auditor registers
   (core/auditor/_year_scope_chip.html). The options are links, so the
   selected one is marked with aria-current, and it is painted with the neutral
   where-you-are fill (secondary-container, not court red). The "under audit"
   note sits under its year, so three options fit on a 390px phone. */
.npsc-year-segmented {
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  gap: 2px;
}
.npsc-year-segmented .m3e-segmented__btn {
  flex: 0 1 auto;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 2px var(--md-sys-spacing-4);
  line-height: 1.2;
  text-decoration: none;
  font-size: 14px;
}
.npsc-year-segmented .m3e-segmented__btn[aria-current="true"] {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  box-shadow: inset 0 0 0 1px var(--md-sys-color-outline);
}
.npsc-year-segmented__note {
  font-size: 11px;
  font-weight: 500;
}

/* Auditor registers switch from table to cards at 1280px, not 840px. Once
   dates, amounts and record numbers stopped breaking inside their cells, the
   invoice, receipt and member registers no longer fit the 674px content column
   at 1024px and scrolled sideways. Below 840px the shared `.m3e-table--cards`
   rules already apply; this block extends the same card recipe up to 1279px
   for tables that carry `.npsc-aud-cards`. */
@media (min-width: 840px) and (max-width: 1279px) {
  .m3e-table--cards.npsc-aud-cards,
  .npsc-aud-cards > tbody,
  .npsc-aud-cards > tbody > tr,
  .npsc-aud-cards > tbody > tr > td {
    display: block;
    width: auto;
  }
  .npsc-aud-cards > thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .npsc-aud-cards > tbody > tr {
    background: var(--md-sys-color-surface);
    border: 1px solid var(--md-sys-color-outline-variant);
    border-radius: var(--md-sys-shape-corner-medium);
    padding: var(--md-sys-spacing-3) var(--md-sys-spacing-4);
    margin-bottom: var(--md-sys-spacing-3);
  }
  .m3e-table.npsc-aud-cards > tbody > tr > td {
    border: none;
    padding: var(--md-sys-spacing-1) 0;
    text-align: start;
  }
  .npsc-aud-cards > tbody > tr > td[data-label] {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--md-sys-spacing-4);
    flex-wrap: wrap;
    row-gap: var(--md-sys-spacing-1);
  }
  .npsc-aud-cards > tbody > tr > td[data-label]::before {
    content: attr(data-label);
    font-family: var(--font-ui);
    font-variant-numeric: normal;
    letter-spacing: normal;
    font-size: 12px;
    font-weight: 500;
    color: var(--md-sys-color-on-surface-variant);
    flex-shrink: 0;
  }
  .npsc-aud-cards > tbody > tr > td.m3e-table__td--numeric { text-align: end; }
  .m3e-table-wrapper:has(> .npsc-aud-cards) {
    overflow-x: visible;
    border-color: transparent;
  }
  .m3e-table-wrapper:has(> .npsc-aud-cards)::after { content: none; }
}

/* A member with no name on record is titled by their email address, one
   unbreakable run that pushed a 390px page 75px sideways. The heading may
   break inside it rather than widen the page. This was scoped to the auditor
   member page; the committee member page (and any other page titled by a
   record's email or reference) hit the same 75px, so it is the page header's
   rule now. Ordinary words still fit their line and never split. */
.m3e-page-header__heading {
  overflow-wrap: anywhere;
}

/* An address is one unit: it never breaks mid-word. It ends in an ellipsis on
   one line, and the whole address is in its title (auditor member register). */
.npsc-aud-email {
  display: block;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The Draw / Type name switch on the auditor signing pages is a choice, not an
   action. The shared module paints the chosen side as a filled primary button,
   the same court red as "Review and confirm", so it read as a second
   submit. On the auditor surface it is a neutral segmented pair. */
.npsc-sig-neutral [role="group"][aria-label="Signature method"] {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: var(--md-sys-shape-corner-small);
  background: var(--md-sys-color-surface-container);
}
.npsc-sig-neutral [data-sig-mode].m3e-button {
  border: none;
  border-radius: var(--md-sys-shape-corner-small);
  background: transparent;
  color: var(--md-sys-color-on-surface-variant);
  box-shadow: none;
}
.npsc-sig-neutral [data-sig-mode].m3e-button[aria-pressed="true"] {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  box-shadow: inset 0 0 0 1px var(--md-sys-color-outline);
}
/* QA 2026-09-25 · governance */
/* Attention rows: the reason never starts a line with "·". On a phone it is
   its own muted line under the title; wider, the separator is glued to both
   neighbours with no-break spaces so it can only travel with them. */
.m3e-attention-item__title + .m3e-attention-item__reason::before {
  content: '\00a0·\00a0';
  white-space: normal;
}
@media (max-width: 839px) {
  .m3e-attention-item__title,
  .m3e-attention-item__title + .m3e-attention-item__reason { display: block; }
  .m3e-attention-item__title + .m3e-attention-item__reason::before { content: none; }
  .m3e-attention-item__meta { margin-left: 44px; }
}
/* Notifications hub: its queues sit in half-width columns, so title and
   reason are two lines (title balanced) and a due date is a third line under
   them, never a nowrap column that squeezes the reason to one word a line. */
[data-command-center] .m3e-attention-item { flex-wrap: wrap; row-gap: 2px; }
[data-command-center] .m3e-attention-item__body { flex-basis: 0; }
[data-command-center] .m3e-attention-item__title,
[data-command-center] .m3e-attention-item__title + .m3e-attention-item__reason { display: block; }
[data-command-center] .m3e-attention-item__title { text-wrap: balance; }
[data-command-center] .m3e-attention-item__reason { text-wrap: pretty; }
[data-command-center] .m3e-attention-item__title + .m3e-attention-item__reason::before { content: none; }
[data-command-center] .m3e-attention-item__meta { order: 5; flex-basis: 100%; margin-left: 0; padding-left: 44px; }
@media (max-width: 839px) {
  [data-command-center] .m3e-attention-item__body { flex-basis: calc(100% - 44px); }
}
/* Membership and notifications: rows of different heights keep the count,
   the title and the "Open →" on one top line instead of centring each. */
[data-command-center] .m3e-attention-item,
[data-tour="committee-membership-gaps-items"] .m3e-attention-item { align-items: flex-start; }
[data-command-center] .m3e-attention-item__action,
[data-tour="committee-membership-gaps-items"] .m3e-attention-item__action { line-height: 32px; }
[data-command-center] .m3e-attention-item__body,
[data-tour="committee-membership-gaps-items"] .m3e-attention-item__body { padding-top: 6px; }
[data-command-center] .m3e-attention-item__reason,
[data-tour="committee-membership-gaps-items"] .m3e-attention-item__reason { text-wrap: pretty; }
/* Committee home queue: title and reason are two lines (as on the
   notifications hub), so the title can balance and never strands "yet". */
[data-committee-home-queue] .m3e-attention-item__title,
[data-committee-home-queue] .m3e-attention-item__title + .m3e-attention-item__reason { display: block; }
[data-committee-home-queue] .m3e-attention-item__title { text-wrap: balance; }
[data-committee-home-queue] .m3e-attention-item__title + .m3e-attention-item__reason::before { content: none; }
/* Audit log pager: "Rows per page" is one label, never two lines. */
[data-audit-pager] label[for$="-page-size"] { white-space: nowrap; }
/* QA 2026-09-25 · governance end */

/* ═══════════════════════════════════════════════════════════════════════════
 * PHONE FIELD — [🇸🇬 +65 ▾][ 9123 4567 ]  (core/phone_field.py, js/phone-field.js)
 *
 * One house control box holding two controls: a compact country picker and
 * the national number. Standalone (the waiver, the members register) the
 * `.m3e-phone-field` IS the box; inside `text_field.html` the input wrapper
 * already is, so the inner field goes flat and fills it. Either way the focus
 * ring and the error edge belong to the whole box, never to one half.
 * ═══════════════════════════════════════════════════════════════════════════ */
.m3e-phone-field {
  display: flex;
  align-items: stretch;
  width: 100%;
  min-width: 0;
  min-height: var(--control-h-form);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--control-fill);
  color: var(--control-ink);
  font-size: var(--control-fs);
  transition: border-color var(--duration-quick) var(--ease-rally);
}
.m3e-phone-field:focus-within { border-color: var(--brand); }
.m3e-phone-field:has(:focus-visible),
.m3e-phone-field:has(.ts-wrapper.focus) {
  outline: var(--m3e-focus-ring-width) solid var(--m3e-focus-ring-color) !important;
  outline-offset: var(--m3e-focus-ring-offset) !important;
}
.m3e-phone-field:has([aria-invalid="true"]) {
  border-color: var(--danger);
  box-shadow: inset 0 0 0 1px var(--danger);
  background-color: color-mix(in srgb, var(--danger-container) 35%, var(--surface-0));
}

/* Inside a text field the wrapper is the box: no second border, no padding of
   its own, and the ring moves out to the wrapper. */
.m3e-text-field__input-wrapper:has(> .m3e-phone-field) { padding-inline: 0; }
.m3e-text-field__input-wrapper > .m3e-phone-field {
  flex: 1 1 auto;
  align-self: stretch;
  min-height: inherit;
  margin-block: -1px;
  border-color: transparent;
  background-color: transparent;
  box-shadow: none;
  outline: none !important;
}
.m3e-text-field__input-wrapper:has(> .m3e-phone-field :focus-visible),
.m3e-text-field__input-wrapper:has(> .m3e-phone-field .ts-wrapper.focus) {
  outline: var(--m3e-focus-ring-width) solid var(--m3e-focus-ring-color) !important;
  outline-offset: var(--m3e-focus-ring-offset) !important;
}

/* The two halves draw nothing of their own. */
.m3e-phone-field .m3e-phone-field__number,
.m3e-phone-field select.m3e-phone-field__country,
.m3e-phone-field .ts-wrapper.single,
.m3e-phone-field .ts-wrapper.single .ts-control {
  border: 0;
  border-radius: 0;
  box-shadow: none !important;
  background-color: transparent;
  min-height: 0;
  margin: 0;
  outline: none !important;
}
.m3e-phone-field .m3e-phone-field__number {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  padding-inline: var(--md-sys-spacing-3) var(--control-pad-x);
  color: var(--control-ink);
  font: inherit;
  font-variant-numeric: tabular-nums;
}
/* The house invalid wash belongs to the box, not to the half that is wrong. */
.m3e-phone-field input.m3e-phone-field__number[aria-invalid="true"]:not([type="checkbox"]) {
  background-color: transparent;
}
.m3e-phone-field .m3e-phone-field__number::placeholder {
  color: var(--md-sys-color-on-surface-variant);
  opacity: 1;
}

/* The picker: flag, dial code and the house chevron, a hairline after it. */
.m3e-phone-field select.m3e-phone-field__country,
.m3e-phone-field .ts-wrapper.single {
  flex: 0 0 auto;
  width: auto;
  max-width: 55%;
  border-inline-end: 1px solid var(--border);
  border-radius: calc(var(--control-radius) - 1px) 0 0 calc(var(--control-radius) - 1px);
  cursor: pointer;
}
/* Native fallback (no JS, and every picker until it is lazily enhanced):
   its closed text is "🇸🇬 Singapore (+65)". A FIXED width, not `auto`: an
   auto-width select is as wide as its longest option ("South Georgia & South
   Sandwich Islands (+500)"), and a percentage max-width does not shrink what
   it contributes to a grid track — signup overflowed a 360px phone by 56px
   until the picker scrolled into view and was replaced. */
.m3e-phone-field select.m3e-phone-field__country {
  width: 9rem;
  min-width: 0;
  padding-inline: var(--control-pad-x) calc(var(--control-pad-x) + 14px);
  text-overflow: ellipsis;
}
.m3e-phone-field .ts-wrapper.single {
  padding-inline: var(--control-pad-x) 0;
  transition: background-color var(--duration-quick) var(--ease-rally);
}
.m3e-phone-field .ts-wrapper.single.focus {
  background-color: color-mix(in srgb, var(--fg1) 6%, transparent);
}
@media (hover: hover) and (pointer: fine) { .m3e-phone-field .ts-wrapper.single:hover { background-color: color-mix(in srgb, var(--fg1) 6%, transparent); } }
.m3e-phone-field .ts-wrapper.single .ts-control {
  /* Room for the chevron. The vendor sheet owns `padding-right` with an
     `!important` in the lower `base` layer, which outranks anything here, and
     reads it from these three properties — so set them, not the padding. */
  --ts-pr-min: calc(var(--md-sys-spacing-2) + var(--control-glyph) + var(--md-sys-spacing-1));
  --ts-pr-caret: 0px;
  --ts-pr-clear-button: 0px;
  padding-left: 0 !important;
  height: 100%;
  cursor: pointer;
}
.m3e-phone-field .ts-wrapper.single::after { right: var(--md-sys-spacing-2); }
.m3e-phone-item {
  display: inline-flex;
  align-items: center;
  gap: var(--md-sys-spacing-2);
  white-space: nowrap;
}
.m3e-phone-item__flag,
.m3e-phone-option__flag {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  font-size: 1.25em;
  line-height: 1;
}
/* The SVG flag (Windows has no emoji flags). A hairline keeps a white flag
   from dissolving into the menu. */
.m3e-phone-flag {
  display: block;
  width: 20px;
  height: 15px;
  border-radius: 2px;
  outline: 1px solid var(--border);
  outline-offset: -1px;
  object-fit: cover;
}
.m3e-phone-item__dial { color: var(--fg1); }

/* In a register cell the column is sized by its content; give the number
   room for a full "9123 4567" beside the picker. */
td .m3e-phone-field { min-width: 14rem; }

/* The dropdown: as wide as the whole field (phone-field.js positions it),
   never wider than the viewport, a scrolling list under a search box. */
.ts-dropdown.m3e-phone-dropdown {
  z-index: var(--z-menu);
  margin-top: 0;
  max-width: calc(100vw - 16px);
}
.ts-dropdown.m3e-phone-dropdown .ts-dropdown-content {
  max-height: min(20rem, 45vh);
  max-height: min(20rem, 45dvh);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.ts-dropdown.m3e-phone-dropdown .dropdown-input-wrap {
  padding: var(--md-sys-spacing-1) var(--md-sys-spacing-2) var(--md-sys-spacing-2);
  border-bottom: 1px solid var(--border);
}
.ts-dropdown.m3e-phone-dropdown .dropdown-input {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0 var(--control-pad-x);
  border: 1px solid var(--control-border);
  border-radius: var(--control-radius);
  background-color: var(--control-fill);
  color: var(--control-ink);
  font-family: inherit;
  font-size: var(--control-fs);
  box-shadow: none;
}
.ts-dropdown.m3e-phone-dropdown .dropdown-input:focus-visible {
  border-color: var(--brand);
  outline-offset: 0 !important;
}
.m3e-phone-option {
  display: flex;
  align-items: center;
  gap: var(--md-sys-spacing-3);
  width: 100%;
  min-width: 0;
}
.m3e-phone-option__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m3e-phone-option__dial {
  flex: none;
  color: var(--fg2);
}
.ts-dropdown.m3e-phone-dropdown .option.active .m3e-phone-option__dial,
.ts-dropdown.m3e-phone-dropdown .option.selected .m3e-phone-option__dial { color: inherit; }
/* Singapore is pinned above "All countries"; the group label is the divider. */
.ts-dropdown.m3e-phone-dropdown .optgroup {
  border-top: 1px solid var(--border);
  margin-top: var(--md-sys-spacing-1);
}
.ts-dropdown.m3e-phone-dropdown .optgroup:first-child {
  border-top: 0;
  margin-top: 0;
}
}

/* Courts paid back: the screenshot reader says what it is doing (Kenny, 2026-09-28). */
[data-read-status][data-read-state] { font-size: 0.9375rem; font-weight: 600; color: var(--fg1); }
[data-read-status][data-read-state="busy"]::before {
  content: ""; display: inline-block; width: 1em; height: 1em; margin-right: 0.5em; vertical-align: -0.15em;
  border: 2px solid var(--border); border-top-color: var(--brand); border-radius: 50%;
  animation: m3e-spin var(--md-sys-motion-duration-spinner) linear infinite;
}
[data-read-status][data-read-state="failed"] { color: var(--danger); }

/* Courts paid back: while reading, the lines below are blurred and locked
   under a clear "Reading the screenshot" card; the chosen screenshot shows
   above them so the filled lines can be checked against it. */
[data-read-zone].is-reading > :not([data-read-overlay]) {
  filter: blur(3px); opacity: 0.45; pointer-events: none; user-select: none;
  /* The blur is applied at once (filter repaints every child on every frame
     it animates — review P2-6); only the dim fades. */
  transition: opacity var(--duration-quick) var(--easing-effects);
}
.m3e-read-overlay { position: absolute; inset: 0; z-index: 5; display: flex; justify-content: center; }
.m3e-read-overlay[hidden] { display: none; }
.m3e-read-overlay::before {
  content: ""; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
}
.m3e-read-overlay__card {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: calc(100% - 3rem);
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem; text-align: center;
  max-width: 18rem; padding: 1.25rem 1.5rem; border-radius: var(--radius-lg, 16px);
  background: var(--surface-1, #fff); color: var(--fg1);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18); border: 1px solid var(--border);
}
.m3e-read-overlay__spinner {
  width: 2rem; height: 2rem; border-radius: 50%;
  border: 3px solid var(--border); border-top-color: var(--brand);
  animation: m3e-spin var(--md-sys-motion-duration-spinner) linear infinite;
}
.m3e-read-preview {
  display: block; width: 100%; max-width: 640px; padding: 0; border: 1px solid var(--border);
  border-radius: var(--radius-md, 12px); background: var(--surface-2, #f5f5f5); overflow: hidden; cursor: zoom-in;
}
.m3e-read-preview img { display: block; width: 100%; max-height: 14rem; object-fit: contain; }
.m3e-read-preview[aria-expanded="true"] { cursor: zoom-out; }
.m3e-read-preview[aria-expanded="true"] img { max-height: none; }
/* A field the reader just filled flashes a ring that fades out. Inputs carry
   no pseudo-elements, so this fades an outline's colour (no blur, no layout)
   rather than a 1.6s box-shadow (review P2-6), inside the overlay budget. */
.m3e-read-filled { animation: m3e-read-filled var(--md-sys-motion-duration-medium4) var(--easing-effects); }
@keyframes m3e-read-filled {
  from { outline: 3px solid var(--brand); outline-offset: 0; }
  to   { outline: 3px solid transparent; outline-offset: 0; }
}

/* Courts paid back: each line says whether the plan needed it. */
[data-line-note][data-plan-state="ok"] { color: var(--fg2); }
[data-line-note][data-plan-state="warn"] { color: var(--warning-on-container); font-weight: 600; }
[data-line-note][data-plan-state="bad"] { color: var(--danger); font-weight: 600; }

/* Courts paid back: stage summary (tap to filter) and screenshot thumbnails. */
.m3e-paid-back-stage {
  display: grid; grid-template-columns: 1fr auto; grid-template-areas: "label label" "amount courts";
  align-items: baseline; row-gap: .25rem; column-gap: .75rem;
  min-height: 44px; padding: .875rem 1rem; border-radius: var(--radius-md);
  border: 1px solid var(--border); border-left: 4px solid var(--brand);
  background: var(--md-sys-color-surface); color: var(--fg1); text-decoration: none;
}
.m3e-paid-back-stage__label { grid-area: label; font-size: .875rem; line-height: 1.25rem; font-weight: 500; color: var(--fg2); }
.m3e-paid-back-stage__amount { grid-area: amount; font-size: 1.5rem; line-height: 2rem; font-weight: 600; color: var(--fg1); font-variant-numeric: tabular-nums; }
.m3e-paid-back-stage__courts { grid-area: courts; font-size: .875rem; line-height: 1.25rem; color: var(--fg2); white-space: nowrap; }
.m3e-paid-back-stage.is-empty { border-left-color: var(--border); }
.m3e-paid-back-stage.is-rejected { border-left-color: var(--danger); }
.m3e-paid-back-stage.is-rejected .m3e-paid-back-stage__label { color: var(--danger); font-weight: 600; }
.m3e-paid-back-stage.is-empty { color: var(--fg2); }
.m3e-paid-back-stage.is-selected { border-color: var(--brand); background: var(--brand-container, var(--surface-2)); box-shadow: inset 0 0 0 1px var(--brand); }
/* .m3e-paid-back-stage focus: the house ring. */
.m3e-paid-back-thumb {
  display: flex; align-items: center; justify-content: center; width: 56px; height: 96px;
  overflow: hidden; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2); color: var(--fg2);
}
.m3e-paid-back-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; }

/* Kenny, 2026-09-29: the two answers on "Add a booking" (court_booking_add.html). */
.m3e-booking-choice {
  position: relative;
  display: flex; flex-direction: column; gap: 0.25rem;
  min-height: 44px; padding: 1rem;
  border: 1.5px solid var(--border-bold); border-radius: var(--radius-md);
  background: var(--md-sys-color-surface); text-decoration: none;
}
.m3e-booking-choice::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: var(--fg1); opacity: 0; pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard-effects-default);
}
@media (hover: hover) and (pointer: fine) { .m3e-booking-choice:hover { border-color: var(--brand); } }
.m3e-booking-choice:active { border-color: var(--brand); }
.m3e-booking-choice:active::before { opacity: 0.16; }
/* .m3e-booking-choice focus: the house ring. */
.m3e-booking-switch {
  display: flex; gap: 0.75rem; align-items: flex-start;
  padding: 0.875rem 1rem; border-radius: var(--radius-md);
  border: 1px solid var(--brand-on-container); border-left-width: 4px;
  background: var(--brand-container); color: var(--brand-on-container);
}
.m3e-booking-switch__body { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; min-width: 0; }
.m3e-booking-switch .m3e-button { background: var(--md-sys-color-surface); }

/* ═══════════════════════════════════════════════════════════════════════════
 * ADDRESS FIELD — postal code first  (core/address_field.py, js/address-field.js)
 *
 * A fieldset of four house text fields: postal code (with the OneMap
 * attribution and a polite status line under it), block and street, then
 * unit beside building name. The boxes are ordinary text fields; this block
 * only lays them out.
 * ═══════════════════════════════════════════════════════════════════════════ */
.m3e-address-fieldset {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.m3e-address-field {
  display: grid;
  gap: var(--md-sys-spacing-4);
  min-width: 0;
}
.m3e-address-field__postal { min-width: 0; }
/* The note and status sit directly under the postal box, so the box gives up
   the reserved error line that form[data-validate] adds under fields. */
form[data-validate] .m3e-address-field__postal > .m3e-text-field { padding-bottom: 0; }
.m3e-address-field__note,
.m3e-address-field__status {
  font-size: 12px;
  line-height: 1.4;
  padding-inline: var(--control-pad-x);
  margin-top: var(--control-gap);
}
.m3e-address-field__note { color: var(--fg2); }
.m3e-address-field__status { color: var(--fg1); }
.m3e-address-field__status:empty { display: none; }
.m3e-address-field__row {
  display: grid;
  gap: var(--md-sys-spacing-4);
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
}
@media (min-width: 640px) {
  .m3e-address-field__postal { max-width: 16rem; }
  .m3e-address-field__row { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }
}
.m3e-address-fieldset > .m3e-text-field__error-text { display: block; }
/* A form that marks nothing optional (address_field.html mark_optional=False). */
.m3e-address-fieldset--unmarked .m3e-optional { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
 * EMAIL TYPO HINT — "Did you mean kenny@gmail.com?"  (js/email-typo.js)
 *
 * Helper-text type under the box; the correction is a text button that puts
 * the fix in the field. The live region exists (empty, taking no room) from
 * the moment the box is focused, so the hint is announced when it arrives.
 * ═══════════════════════════════════════════════════════════════════════════ */
.m3e-email-typo {
  font-size: 12px;
  line-height: 1.4;
  color: var(--fg2);
  padding-inline: var(--control-pad-x);
  margin-top: var(--control-gap);
}
.m3e-email-typo:empty { margin: 0; padding: 0; }
.m3e-text-field > .m3e-email-typo { margin-top: 0; }
.m3e-email-typo__button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  max-width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: var(--brand);
  font: inherit;
  font-weight: 500;
  text-align: start;
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .m3e-email-typo__button:hover { text-decoration-thickness: 2px; } }

/* ═══════════════════════════════════════════════════════════════════════════
 * SIGNUP REVIEW — one section per step, each with an "Edit" back to it
 * (js/pages/auth.js renderReview). Rows keep their hairline; a section head
 * is the step's name and a text button.
 * ═══════════════════════════════════════════════════════════════════════════ */
.signup-review__section + .signup-review__section { margin-top: var(--md-sys-spacing-3); }
.signup-review__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-sys-spacing-3);
}
.signup-review__title {
  margin: 0;
  font-size: var(--md-sys-typescale-title-small-size, 14px);
  font-weight: 600;
  color: var(--fg1);
}
.signup-review__edit {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  min-width: 44px;
  justify-content: flex-end;
  padding: 0;
  border: 0;
  background: none;
  color: var(--brand);
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) { .signup-review__edit:hover { text-decoration-thickness: 2px; } }
/* A value takes the width it needs up to 60% of the row, and only a value
   longer than that (an address, a long email) wraps — never "Y / es". */
.signup-review__value { flex: 0 0 auto; max-width: 60%; overflow-wrap: anywhere; }

@layer m3e {
/* A typo hint and a green "valid" tick on the same box contradict each other:
   while "Did you mean …?" is showing, the field reads as neutral. */
.m3e-text-field:has(> .m3e-email-typo:not(:empty)) .validation-success-icon { display: none; }
.m3e-text-field:has(> .m3e-email-typo:not(:empty)) .m3e-text-field__input-wrapper:has(> .npsc-field-valid) {
  border-color: var(--control-border) !important;
}


/* ═══════════════════════════════════════════════════════════════════════════
 * MOBILE-NATIVE FLOOR (ds-reports/mobile-native.md, MN-03 … MN-12)
 * Platform-layer rules, stated once. Gate by capability, never by width.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* MN-03: a tap on a control fires click immediately — it never waits to see
   whether a double-tap zoom is coming. Drag surfaces state their own
   `touch-action` (none / pan-y) with higher specificity and keep it. */
a[href], button, [role="button"], summary, label, select,
input:is([type="button"], [type="submit"], [type="reset"], [type="checkbox"], [type="radio"]) {
  touch-action: manipulation;
}

/* MN-04: a long press on a CONTROL neither selects its label nor pops the
   link callout. Content stays selectable: prose links, status chips
   (`[data-court-status]` is text), table cells and every paragraph. */
.m3e-button, .m3e-icon-button, .m3e-fab, .m3e-chip:not([data-court-status]),
.m3e-tabs__tab, .m3e-segmented__btn, .m3e-nav-bar__item, .m3e-nav-drawer__item,
.m3e-menu__item, .m3e-top-app-bar__icon-btn, .m3e-pagination a,
.m3e-bottom-sheet__drag-handle-wrapper, #mobile-menu-panel nav a {
  -webkit-user-select: none;
  user-select: none;
}
/* The long-press callout is suppressed on non-link controls only: on a link
   (a PDF button, a drawer row) it is Open in New Tab / Copy Link / Share,
   which an installed app with no browser chrome still needs. */
:is(.m3e-button, .m3e-icon-button, .m3e-fab, .m3e-chip, .m3e-tabs__tab,
    .m3e-segmented__btn, .m3e-menu__item, .m3e-top-app-bar__icon-btn,
    .m3e-bottom-sheet__drag-handle-wrapper):not(a) {
  -webkit-touch-callout: none;
}

/* MN-05: press feedback on the most-tapped controls that had none. With the
   tap highlight gone (preflight), these showed nothing until navigation. */
.m3e-nav-bar__item { -webkit-tap-highlight-color: transparent; }
.m3e-nav-bar__item:active .m3e-nav-bar__indicator {
  background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
  transition-duration: var(--md-sys-motion-duration-short2);
}
.m3e-nav-bar__item--active:active .m3e-nav-bar__indicator {
  background-color: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 16%, var(--md-sys-color-secondary-container));
}
.m3e-top-app-bar__icon-btn:active::before { opacity: 0.16; }
.m3e-pagination a { transition: background-color var(--md-sys-motion-duration-short2) var(--easing-effects); }
/* !important: the prev/next links carry a Tailwind `bg-[…]` utility, and
   @layer utilities beats @layer m3e, so without it these never showed. */
.m3e-pagination a:active { background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 16%, var(--surface-2)) !important; }
#mobile-menu-panel nav a { transition: background-color var(--md-sys-motion-duration-short2) var(--easing-effects); }
#mobile-menu-panel nav a:active { background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent) !important; }

/* MN-11: content under the notch. `viewport-fit=cover` is on, so the fixed
   chrome pads itself out of the sensor housing and the home-indicator zone,
   including the left/right insets of a phone in landscape. */
.m3e-top-app-bar--fixed {
  padding-top: env(safe-area-inset-top, 0px);
  padding-left: max(var(--md-sys-spacing-1), env(safe-area-inset-left, 0px));
  padding-right: max(var(--md-sys-spacing-1), env(safe-area-inset-right, 0px));
}
.m3e-shell__content {
  padding-top: calc(var(--shell-top-bar-height) + env(safe-area-inset-top, 0px));
  padding-right: env(safe-area-inset-right, 0px);
}
/* The left inset belongs to whatever sits at the left edge: the permanent
   drawer when there is one (a landscape iPhone gets the >=840px layout), the
   content column otherwise. */
body:not(:has(.m3e-shell__drawer--permanent)) .m3e-shell__content {
  padding-left: env(safe-area-inset-left, 0px);
}
.m3e-shell__drawer--permanent {
  padding-left: env(safe-area-inset-left, 0px);
}
.m3e-shell__bottom-nav {
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

/* MN-12: inner scrollers keep their own bounce but never chain it to the
   page; horizontal strips never hand an over-swipe to history navigation. */
.m3e-account-menu,
.ts-dropdown .ts-dropdown-content,
.m3e-nav-drawer__body,
.m3e-table-wrapper--vscroll,
.m3e-menu {
  overscroll-behavior: contain;
}
.m3e-hscroll,
.m3e-table-wrapper,
[data-hscroll],
.m3e-kpi-strip {
  overscroll-behavior-x: contain;
}

/* MN-08: the software keyboard. While a text control has focus on a touch
   phone, the bottom tab bar is noise between the field and the keys: hide it,
   and let the sticky form bar and the snackbar drop to the safe-area edge so
   Save rides on top of the keyboard (`interactive-widget=resizes-content`
   resizes the layout viewport on Android; iOS overlays and scrolls). */
@media (max-width: 839px) and (pointer: coarse) {
  /* `m3e-keyboard-open` is set by m3e-shell.js only while a text control has
     focus AND the visual viewport has shrunk for the keyboard, so a focused
     field with no keyboard (Android back-gesture close, a live-filter search
     being read, a script-focused MFA field) keeps the tab bar. */
  body.m3e-keyboard-open .m3e-shell__bottom-nav {
    display: none;
  }
  body.m3e-keyboard-open .m3e-form-actions {
    bottom: env(safe-area-inset-bottom, 0px);
  }
  body.m3e-keyboard-open .m3e-snackbar-container--bottom-center {
    bottom: env(safe-area-inset-bottom, 0px);
  }
  body.m3e-keyboard-open:has(.m3e-form-actions) .m3e-snackbar-container--bottom-center {
    bottom: calc(env(safe-area-inset-bottom, 0px) + var(--m3e-snackbar-form-actions-lift));
  }
}

}
