/* ============================================================================
   MPA PLATFORM DESIGN SYSTEM — Stage A foundation (§9 aesthetic pass)
   ----------------------------------------------------------------------------
   One central token + component layer. Pages consume these tokens/classes;
   page templates should NOT redefine their own copies (the Dashboard and
   Reports prototypes still carry scoped duplicates — they migrate to these
   classes in Stage B).

   BRAND TOKENS (--mpa-action, --mpa-tint, --mpa-drawer-*, …) are injected
   per-request by base.html from app/services/branding.py derive_brand_tokens()
   — contrast-safe derivations of the school's Settings → Branding colours.
   The values below are only fallbacks (default charcoal) in case the inline
   block is ever missing. Raw school colours are NEVER used directly here:
   that constraint is what keeps a garish Settings choice from wrecking the UI.

   SEMANTIC colours (ok/warn/danger/info/neutral) are product-controlled and
   never follow school branding.
   ========================================================================== */

:root {
  /* ── Fixed product tokens ── */
  --mpa-bg: #f7f6fa;              /* app canvas: warm off-white / pale lavender */
  --mpa-surface: #ffffff;         /* cards */
  --mpa-ink: #201d29;             /* primary text: warm charcoal */
  --mpa-muted: #746f81;           /* secondary text */
  --mpa-line: #eae7f1;            /* card borders */
  --mpa-hairline: #f3f1f8;        /* row separators, recessive grid */
  --mpa-track: #edebf3;           /* segmented-control track, meters */

  --mpa-ok: #2e9e6b;      --mpa-ok-bg: #e3f3eb;      --mpa-ok-ink: #23744f;
  /* warn-ink: #856417 (18 Oct 2026; was #96731f, 4.40:1 on white) — 5.5:1 on
     white and 5.0:1 on warn-bg, in line with the other -ink tokens (WCAG AA
     for small text). */
  --mpa-warn: #d9822b;    --mpa-warn-bg: #fdf3e4;    --mpa-warn-ink: #856417;
  --mpa-danger: #cf3f56;  --mpa-danger-bg: #fbe7ea;  --mpa-danger-ink: #a32e41;
  --mpa-info: #2e7fc2;    --mpa-info-bg: #eef4fb;    --mpa-info-ink: #35618c;
  /* neutral-ink: #6b6779 (UI/UX pass 1, 25 Oct 2026; was #726e7e, 4.21:1 on
     neutral-bg — Draft chips, term pills) — 4.65:1 on neutral-bg, 5.47:1 on
     white, same hue. */
  --mpa-neutral: #a7a3b3; --mpa-neutral-bg: #edecf2; --mpa-neutral-ink: #6b6779;
  /* Trial enrolments (product-controlled, semantic like ok/warn/danger —
     the timetable's long-standing trial purple, tokenised in §18 S3) */
  --mpa-trial: #6f42c1;   --mpa-trial-bg: #f0e9fa;   --mpa-trial-ink: #5a3796;

  --mpa-radius: 16px;             /* major cards */
  --mpa-radius-sm: 10px;          /* buttons, inputs, small tiles */
  --mpa-shadow: 0 1px 2px rgba(32,25,60,.03), 0 12px 32px -20px rgba(32,25,60,.18);
  --mpa-shadow-lift: 0 2px 4px rgba(32,25,60,.04), 0 18px 40px -20px rgba(32,25,60,.25);

  --mpa-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* ── Type scale (UI-CONSISTENCY-1, 21 Sep 2026) ──
     Until now :root tokenised colour, radius, shadow and font FAMILY but
     never font SIZE, and the audit measured the result: 61 literal
     font-size values in this file and 103 distinct rendered type
     signatures, against ~4 rendered radii for the axis that IS
     tokenised. This is a conservative scale whose steps are the values
     the reference pages (Students, Classes, Dashboard, Admin Console)
     already render, so adopting it changes nothing on those pages and
     collapses the accidental .66/.68/.7/.72 and .74/.76/.78/.8/.82
     neighbours everywhere else onto the same step. Desktop (>=992px)
     keeps its 10.5px micro-label floor via max(); see the density block. */
  --mpa-text-2xs: .62rem;     /* legend / superscript micro copy only */
  --mpa-text-xs: .7rem;       /* uppercase micro labels: table heads, KPI/stat/card/filter labels, chips */
  --mpa-text-sm: .8rem;       /* meta, notes, subtitles, row side text */
  --mpa-text-md: .855rem;     /* list/table body, form copy */
  --mpa-text-base: .92rem;    /* body copy, buttons, controls, desktop table body */
  --mpa-text-lg: 1.05rem;     /* lead copy, panel titles */
  --mpa-text-xl: 1.6rem;      /* page title, stat headline */
  --mpa-text-2xl: 2.05rem;    /* KPI figure */
  --mpa-tracking-label: .08em; /* the ONE letter-spacing for uppercase micro labels */

  /* ── Vertical rhythm (UI-CONSISTENCY-1B final, 22 Sep 2026) ──
     Three gaps, used by every shared layout class below, so a page's
     vertical rhythm is a property of the design system and never a
     per-page margin: block→block (card → toolbar → filter bar → list),
     block→section heading, and section heading→its content. */
  --mpa-space-block: 1rem;
  --mpa-space-section: 1.35rem;
  --mpa-space-label: .55rem;

  /* Chart theme (marks wear brand/semantic colour; text wears text tokens) */
  --mpa-chart-deemph: #d9d4e3;    /* context series */
  --mpa-chart-grid: #f0eef5;      /* hairline gridlines */

  /* AI assistant accent — product-controlled, deliberately NOT the school
     colour so the AI feature reads as its own distinctive thing. */
  --ai-assistant-accent: #D97A34;
  /* The launcher's own glow, tokenised (AI-SUPPORT-V1-A) so the widget's
     two box-shadow rules stop hardcoding the orange and follow whichever
     accent the plane is wearing. Values are the exact rgba() pair those
     rules carried before, so the orange launcher is byte-identical. */
  --ai-assistant-glow: 0 6px 20px rgba(217, 122, 52, .35);
  --ai-assistant-glow-hover: 0 8px 24px rgba(217, 122, 52, .45);

  /* Solis SUPPORT AI accent (AI-SUPPORT-V1-A, 20 Sep 2026) — the green
     the setup/support launcher and panel wear. base.html overrides
     --ai-assistant-accent with this on the widget root when the support
     plane is active, so ONE widget and ONE set of rules serve both.

     Chosen, not borrowed: a deep support green at 5.2:1 against white
     (the incumbent orange manages 3.1:1, so this is a contrast
     improvement, not a regression), in the broadly familiar help/chat
     green family but deliberately nowhere near any messaging app's brand
     colour — no logo, no mimicry. Like the orange it is
     product-controlled and never derived from the school's palette: the
     point of both is to read as "this is a Solis thing". */
  --support-ai-accent: #0B7C57;
  --support-ai-glow: 0 6px 20px rgba(11, 124, 87, .35);
  --support-ai-glow-hover: 0 8px 24px rgba(11, 124, 87, .45);

  /* ── Brand tokens: fallback values only (see header comment) ──
     CASCADE WARNING: base.html re-declares every one of these on :root in
     an inline <style> that MUST be placed AFTER this stylesheet's <link>.
     Custom properties at equal specificity resolve by document order, so
     if the order ever flips these charcoal fallbacks win and the whole
     app silently de-brands (grey drawer, charcoal buttons). ── */
  --mpa-brand-raw: #37383A;       /* raw school primary — small accents only */
  --mpa-action: #37383a;          /* buttons/active states; ≥4.5:1 vs white */
  --mpa-action-hover: #2b2b2d;
  --mpa-action-rgb: 55, 56, 58;   /* for alpha rings: rgba(var(--mpa-action-rgb), .2) */
  --mpa-on-action: #ffffff;       /* text on action surfaces (guaranteed) */
  --mpa-ink-brand: #37383a;       /* brand-coloured text/links on white */
  --mpa-action-2: #6c757d;        /* school secondary, contrast-safe */
  --mpa-tint: #efefef;            /* raw-hue wash: icon tiles, soft bg */
  --mpa-tint-strong: #dfdfdf;     /* hover/selected wash */
  --mpa-drawer-bg: #2d2d2f;       /* deep drawer shade; ≥7:1 white text */
  --mpa-drawer-top: #38393b;      /* drawer gradient top */
  --mpa-drawer-accent: #bbbcbe;   /* pastel brand accent on the drawer */

  /* ── Bootstrap bridge (§9.3 root-cause fix): utilities and components
       that read Bootstrap's own variables now track the design system. ── */
  --bs-primary: var(--mpa-action);
  --bs-primary-rgb: var(--mpa-action-rgb);
  --bs-link-color: var(--mpa-ink-brand);
  --bs-link-hover-color: var(--mpa-action-hover);
  --bs-link-color-rgb: var(--mpa-action-rgb);
  --bs-link-hover-color-rgb: var(--mpa-action-rgb);
  --bs-body-bg: var(--mpa-bg);
  --bs-body-color: var(--mpa-ink);
  --bs-body-font-family: var(--mpa-font);
  --bs-border-radius: var(--mpa-radius-sm);
  /* UI-CONSISTENCY-1: the SMALL radius too -- .btn-sm / .form-*-sm /
     input-group corners read --bs-border-radius-sm (Bootstrap's 3.25px),
     which is where the last un-bridged 3.25px corners were coming from. */
  --bs-border-radius-sm: var(--mpa-radius-sm);
  --bs-focus-ring-color: rgba(var(--mpa-action-rgb), .22);
}

/* ============================================================
   BASE & TYPOGRAPHY
   ============================================================ */
body {
  background-color: var(--mpa-bg);
  color: var(--mpa-ink);
  font-family: var(--mpa-font);
  padding-bottom: env(safe-area-inset-bottom, 0); /* notched-device PWA */
}
h1, h2, h3, h4, h5, h6 { letter-spacing: -.01em; }

/* Prevent iOS zoom on input focus — Safari zooms when input font-size < 16px */
@supports (-webkit-touch-callout: none) {
  input, select, textarea { font-size: max(16px, 1em); }
}

/* Layout: main content pane. Extra bottom padding reserves space for the
   fixed AI Assistant widget so scrolled-to-bottom content never sits under it. */
/* min-width:0 is load-bearing (§18 S5 round 5): .main-content is a
   Bootstrap .col — a FLEX ITEM, whose default min-width:auto refuses to
   shrink below its content's min-content width. Any nowrap row inside
   (the mobile tabs row, the day strip) therefore used to widen the whole
   column past the viewport and the page itself scrolled horizontally —
   while every inner overflow-x:auto scroller stayed inert, because a
   scroller only engages when its container is narrower than its content.
   With min-width:0 the column holds the viewport width and the inner
   scrollers do their job (this also lets the desktop grid's own scroll
   panel engage on narrow windows instead of stretching the page). */
/* iOS installed-PWA safe-area top inset (final polish pass, 2 Sep 2026):
   base.html already sets viewport-fit=cover + apple-mobile-web-app-
   status-bar-style=black-translucent, so in STANDALONE mode (added to the
   Home Screen) the page genuinely renders UNDER the status bar/notch/
   Dynamic Island — Rory's finding: the hamburger + page title (the very
   first thing inside .main-content on every page — .mpa-page-head is
   always its first child) sat too close to it. ONE shared rule, here,
   because every page in the app extends base.html and shares this exact
   class — never patched per page.
   calc(2rem + env(...)), NOT a flat override: env(safe-area-inset-top)
   is 0 in every context that doesn't need it (a normal browser tab, an
   Android install, desktop) — the browser/OS itself already reserves
   that area, so this stays byte-identical to the existing 2rem there,
   and only ADDS the exact inset amount on a real notch/Dynamic-Island
   device in standalone mode. The >=992px override below replaces this
   rule's `padding` shorthand wholesale with a flat desktop value that
   carries no env() at all, so desktop is completely unaffected by
   design, not by coincidence. */
.main-content { padding: 2rem; padding-top: calc(2rem + env(safe-area-inset-top, 0px)); padding-bottom: 6rem; min-width: 0; }

[x-cloak] { display: none !important; }

/* ============================================================
   BOOTSTRAP COMPONENT BRIDGE
   Buttons/forms/tabs compiled with literal colours get re-pointed
   at the token system here, once, centrally.
   ============================================================ */
.btn-primary {
  --bs-btn-bg: var(--mpa-action);
  --bs-btn-border-color: var(--mpa-action);
  --bs-btn-color: var(--mpa-on-action);
  --bs-btn-hover-bg: var(--mpa-action-hover);
  --bs-btn-hover-border-color: var(--mpa-action-hover);
  --bs-btn-hover-color: var(--mpa-on-action);
  --bs-btn-active-bg: var(--mpa-action-hover);
  --bs-btn-active-border-color: var(--mpa-action-hover);
  --bs-btn-active-color: var(--mpa-on-action);
  --bs-btn-disabled-bg: var(--mpa-action);
  --bs-btn-disabled-border-color: var(--mpa-action);
  --bs-btn-disabled-color: var(--mpa-on-action);
  --bs-btn-focus-shadow-rgb: var(--mpa-action-rgb);
  border-radius: var(--mpa-radius-sm);
  font-weight: 600;
}
.btn-outline-primary {
  --bs-btn-color: var(--mpa-ink-brand);
  --bs-btn-border-color: var(--mpa-action);
  --bs-btn-hover-bg: var(--mpa-action);
  --bs-btn-hover-border-color: var(--mpa-action);
  --bs-btn-hover-color: var(--mpa-on-action);
  --bs-btn-active-bg: var(--mpa-action);
  --bs-btn-active-border-color: var(--mpa-action);
  --bs-btn-active-color: var(--mpa-on-action);
  --bs-btn-focus-shadow-rgb: var(--mpa-action-rgb);
  border-radius: var(--mpa-radius-sm);
  font-weight: 600;
}
/* School SECONDARY colour: secondary buttons only (used sparingly).
   NOTE: Bootstrap's .text-secondary/.bg-secondary utilities stay Bootstrap
   grey — product neutral — they are NOT the school's secondary colour. */
.btn-secondary {
  --bs-btn-bg: var(--mpa-action-2);
  --bs-btn-border-color: var(--mpa-action-2);
  --bs-btn-color: #fff;
  --bs-btn-hover-bg: var(--mpa-action-2);
  --bs-btn-hover-border-color: var(--mpa-action-2);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--mpa-action-2);
  --bs-btn-active-color: #fff;
  border-radius: var(--mpa-radius-sm);
  font-weight: 600;
}
.btn-secondary:hover { filter: brightness(.92); }
/* Neutral outline buttons: soft, calm, never harsh grey-on-grey */
.btn-outline-secondary {
  --bs-btn-color: var(--mpa-muted);
  --bs-btn-border-color: var(--mpa-line);
  --bs-btn-hover-bg: var(--mpa-hairline);
  --bs-btn-hover-border-color: var(--mpa-line);
  --bs-btn-hover-color: var(--mpa-ink);
  --bs-btn-active-bg: var(--mpa-track);
  --bs-btn-active-border-color: var(--mpa-line);
  --bs-btn-active-color: var(--mpa-ink);
  border-radius: var(--mpa-radius-sm);
}

/* ── UI-CONSISTENCY-1 (21 Sep 2026): the REST of the button bridge ──
   The audit counted 16 Bootstrap button variants in the templates and
   found only the four above re-pointed at the house geometry; the other
   twelve (174 uses across 63 templates) fell through to raw Bootstrap and
   rendered at 3.25px corners / weight 400 beside 10px / 600 house buttons
   in the same row. Every variant now shares ONE geometry (radius, weight,
   focus ring behaviour) and the semantic ones wear the house semantic
   tokens instead of Bootstrap's palette, so "this is bad" is the same red
   on every page. Colour MEANING is preserved throughout: danger is still
   red, success still green, warning still amber. */
.btn-danger, .btn-outline-danger,
.btn-warning, .btn-outline-warning,
.btn-success, .btn-outline-success,
.btn-info, .btn-outline-info,
.btn-light, .btn-outline-light,
.btn-dark, .btn-outline-dark,
.btn-link {
  border-radius: var(--mpa-radius-sm);
  font-weight: 600;
}
.btn-danger {
  --bs-btn-bg: var(--mpa-danger);           --bs-btn-border-color: var(--mpa-danger);
  --bs-btn-color: #fff;
  --bs-btn-hover-bg: var(--mpa-danger-ink); --bs-btn-hover-border-color: var(--mpa-danger-ink);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--mpa-danger-ink); --bs-btn-active-border-color: var(--mpa-danger-ink);
  --bs-btn-active-color: #fff;
  --bs-btn-disabled-bg: var(--mpa-danger);  --bs-btn-disabled-border-color: var(--mpa-danger);
  --bs-btn-focus-shadow-rgb: 207, 63, 86;
}
.btn-outline-danger {
  --bs-btn-color: var(--mpa-danger-ink);    --bs-btn-border-color: var(--mpa-danger);
  --bs-btn-hover-bg: var(--mpa-danger);     --bs-btn-hover-border-color: var(--mpa-danger);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--mpa-danger-ink); --bs-btn-active-border-color: var(--mpa-danger-ink);
  --bs-btn-active-color: #fff;
  --bs-btn-focus-shadow-rgb: 207, 63, 86;
}
/* Warning keeps DARK text: the house amber (#d9822b) clears 4.5:1 against
   the ink but not against white, exactly as Bootstrap's own yellow does. */
.btn-warning {
  --bs-btn-bg: var(--mpa-warn);             --bs-btn-border-color: var(--mpa-warn);
  --bs-btn-color: var(--mpa-ink);
  --bs-btn-hover-bg: #c9741f;               --bs-btn-hover-border-color: #c9741f;
  --bs-btn-hover-color: var(--mpa-ink);
  --bs-btn-active-bg: #c9741f;              --bs-btn-active-border-color: #c9741f;
  --bs-btn-active-color: var(--mpa-ink);
  --bs-btn-disabled-bg: var(--mpa-warn);    --bs-btn-disabled-border-color: var(--mpa-warn);
  --bs-btn-focus-shadow-rgb: 217, 130, 43;
}
.btn-outline-warning {
  --bs-btn-color: var(--mpa-warn-ink);      --bs-btn-border-color: var(--mpa-warn);
  --bs-btn-hover-bg: var(--mpa-warn);       --bs-btn-hover-border-color: var(--mpa-warn);
  --bs-btn-hover-color: var(--mpa-ink);
  --bs-btn-active-bg: var(--mpa-warn);      --bs-btn-active-border-color: var(--mpa-warn);
  --bs-btn-active-color: var(--mpa-ink);
  --bs-btn-focus-shadow-rgb: 217, 130, 43;
}
/* Success uses the darker ok-ink as its fill so white text keeps >= 4.5:1
   (the mid green --mpa-ok manages 3.4:1, which is fine for a chip's
   tinted background but not for white-on-solid). */
.btn-success {
  --bs-btn-bg: var(--mpa-ok-ink);           --bs-btn-border-color: var(--mpa-ok-ink);
  --bs-btn-color: #fff;
  --bs-btn-hover-bg: #1c5f41;               --bs-btn-hover-border-color: #1c5f41;
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: #1c5f41;              --bs-btn-active-border-color: #1c5f41;
  --bs-btn-active-color: #fff;
  --bs-btn-disabled-bg: var(--mpa-ok-ink);  --bs-btn-disabled-border-color: var(--mpa-ok-ink);
  --bs-btn-focus-shadow-rgb: 46, 158, 107;
}
.btn-outline-success {
  --bs-btn-color: var(--mpa-ok-ink);        --bs-btn-border-color: var(--mpa-ok);
  --bs-btn-hover-bg: var(--mpa-ok-ink);     --bs-btn-hover-border-color: var(--mpa-ok-ink);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--mpa-ok-ink);    --bs-btn-active-border-color: var(--mpa-ok-ink);
  --bs-btn-active-color: #fff;
  --bs-btn-focus-shadow-rgb: 46, 158, 107;
}
.btn-info {
  --bs-btn-bg: var(--mpa-info);             --bs-btn-border-color: var(--mpa-info);
  --bs-btn-color: #fff;
  --bs-btn-hover-bg: var(--mpa-info-ink);   --bs-btn-hover-border-color: var(--mpa-info-ink);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--mpa-info-ink);  --bs-btn-active-border-color: var(--mpa-info-ink);
  --bs-btn-active-color: #fff;
  --bs-btn-focus-shadow-rgb: 46, 127, 194;
}
.btn-outline-info {
  --bs-btn-color: var(--mpa-info-ink);      --bs-btn-border-color: var(--mpa-info);
  --bs-btn-hover-bg: var(--mpa-info);       --bs-btn-hover-border-color: var(--mpa-info);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--mpa-info-ink);  --bs-btn-active-border-color: var(--mpa-info-ink);
  --bs-btn-active-color: #fff;
  --bs-btn-focus-shadow-rgb: 46, 127, 194;
}
/* A link-shaped button reads like the house .mpa-link: no underline at
   rest, underline on hover; colour already tracks --bs-link-color. */
.btn-link { text-decoration: none; }
.btn-link:hover, .btn-link:focus-visible { text-decoration: underline; }

/* ── UI-CONSISTENCY-1: SURFACE BRIDGE — cards and tables ──
   ROOT CAUSE of the "grey Excel table" and "grey card" complaints, as
   measured by the audit: :root maps --bs-body-bg to the PAGE CANVAS
   (--mpa-bg, #f7f6fa), and Bootstrap's .card and .table both default
   their own background variables to var(--bs-body-bg). So every
   un-migrated Bootstrap card and every <td> in the product painted the
   canvas colour on top of a white card. The team had diagnosed exactly
   this chain before (see .mpa-table-stack's own comment) but fixed it for
   one class at a time. These two blocks fix it at the token, once.

   Bootstrap declares --bs-card-* ON .card and --bs-table-* ON .table (not
   on :root), so the re-pointing has to happen at the same selector, and
   it wins on source order alone -- no !important, no specificity games. */
.card {
  --bs-card-bg: var(--mpa-surface);
  --bs-card-border-color: var(--mpa-line);
  --bs-card-border-radius: var(--mpa-radius);
  --bs-card-inner-border-radius: calc(var(--mpa-radius) - 1px);
  --bs-card-cap-bg: transparent;
  --bs-card-cap-color: var(--mpa-muted);
  --bs-card-cap-padding-y: .85rem;
  --bs-card-cap-padding-x: 1.25rem;
  --bs-card-spacer-x: 1.25rem;
  --bs-card-spacer-y: 1rem;
  box-shadow: var(--mpa-shadow);
}
/* A Bootstrap card that opted into Bootstrap's own shadow utility gets
   the house shadow instead -- same family, not a heavier second one. */
.card.shadow-sm, .card.shadow { box-shadow: var(--mpa-shadow) !important; }
.card.shadow-none { box-shadow: none !important; }
/* Legacy card heading == house card heading (.mpa-card-title): uppercase
   micro label, muted, brand-ink icon, no grey cap strip and no rule under
   it. Interactive things that sit in a header (a button, a chip, a badge,
   a select) keep their own type -- text-transform inherits, so they are
   reset explicitly. Deliberate coloured caps (.bg-warning-subtle etc.)
   are Bootstrap !important utilities and survive untouched. */
.card > .card-header {
  border-bottom: 0;
  font-size: max(var(--mpa-text-xs), 10.5px);
  font-weight: 700;
  letter-spacing: var(--mpa-tracking-label);
  text-transform: uppercase;
  color: var(--mpa-muted);
  padding-bottom: .35rem;
}
.card > .card-header > i, .card > .card-header .bi:first-child { color: var(--mpa-ink-brand); font-size: var(--mpa-text-base); }
.card > .card-header .btn, .card > .card-header .badge, .card > .card-header .mpa-chip,
.card > .card-header .form-control, .card > .card-header .form-select, .card > .card-header .form-check,
.card > .card-header .dropdown-menu, .card > .card-header .nav, .card > .card-header .small,
.card > .card-header small, .card > .card-header .fw-normal, .card > .card-header .text-muted {
  text-transform: none; letter-spacing: normal;
}
.card > .card-header .btn { font-size: var(--bs-btn-font-size); }
.card > .card-header .badge { font-size: var(--bs-badge-font-size); }
.card > .card-header .mpa-chip { font-size: var(--mpa-text-xs); }
.card > .card-header + .card-body { padding-top: .35rem; }
.card > .card-header.bg-warning-subtle, .card > .card-header.bg-danger-subtle,
.card > .card-header.bg-success-subtle, .card > .card-header.bg-info-subtle {
  padding-bottom: var(--bs-card-cap-padding-y);
}
.card > .card-footer { background: transparent; border-top: 1px solid var(--mpa-hairline); }

.table {
  --bs-table-bg: var(--mpa-surface);             /* WHITE. Never the canvas. */
  --bs-table-color: var(--mpa-ink);
  --bs-table-border-color: var(--mpa-hairline);
  --bs-table-hover-bg: var(--mpa-row-hover);     /* the ONE row hover tint */
  --bs-table-hover-color: var(--mpa-ink);
  --bs-table-striped-bg: var(--mpa-row-hover);
  --bs-table-striped-color: var(--mpa-ink);
  --bs-table-active-bg: var(--mpa-track);
  --bs-table-active-color: var(--mpa-ink);
  vertical-align: middle;                         /* one cell alignment */
  font-size: var(--mpa-text-md);                  /* house table body */
}
.table > thead { vertical-align: middle; }
/* Legacy table headings == .mpa-table headings: uppercase micro label on
   a WHITE ground with a soft rule, not Bootstrap's 13px sentence-case on
   #f8f9fa. .table-light (the grey header strip 32 templates reach for) is
   re-pointed too, so adding it no longer paints grey. */
.table > thead > tr > th {
  font-size: max(var(--mpa-text-xs), 10.5px);
  font-weight: 700;
  letter-spacing: var(--mpa-tracking-label);
  text-transform: uppercase;
  color: var(--mpa-muted);
  border-bottom: 1px solid var(--mpa-line);
}
.table-light, .table > .table-light {
  --bs-table-bg: var(--mpa-surface);
  --bs-table-color: var(--mpa-muted);
  --bs-table-border-color: var(--mpa-line);
  --bs-table-hover-bg: var(--mpa-surface);
  --bs-table-striped-bg: var(--mpa-surface);
}
.table > tfoot {
  --bs-table-bg: var(--mpa-row-hover);
  border-top: 1px solid var(--mpa-line);
  font-variant-numeric: tabular-nums;
}
/* Semantic row/cell tints keep their MEANING, on the house palette: an
   admin-fee row is still amber, a discount row still green, a 30-day
   overdue row still rose -- but the same amber/green/rose as every chip. */
.table-warning   { --bs-table-bg: var(--mpa-warn-bg);    --bs-table-color: var(--mpa-ink); --bs-table-border-color: var(--mpa-hairline); --bs-table-hover-bg: var(--mpa-warn-bg);    --bs-table-hover-color: var(--mpa-ink); --bs-table-striped-bg: var(--mpa-warn-bg); }
.table-danger    { --bs-table-bg: var(--mpa-danger-bg);  --bs-table-color: var(--mpa-ink); --bs-table-border-color: var(--mpa-hairline); --bs-table-hover-bg: var(--mpa-danger-bg);  --bs-table-hover-color: var(--mpa-ink); --bs-table-striped-bg: var(--mpa-danger-bg); }
.table-success   { --bs-table-bg: var(--mpa-ok-bg);      --bs-table-color: var(--mpa-ink); --bs-table-border-color: var(--mpa-hairline); --bs-table-hover-bg: var(--mpa-ok-bg);      --bs-table-hover-color: var(--mpa-ink); --bs-table-striped-bg: var(--mpa-ok-bg); }
.table-info      { --bs-table-bg: var(--mpa-info-bg);    --bs-table-color: var(--mpa-ink); --bs-table-border-color: var(--mpa-hairline); --bs-table-hover-bg: var(--mpa-info-bg);    --bs-table-hover-color: var(--mpa-ink); --bs-table-striped-bg: var(--mpa-info-bg); }
.table-secondary { --bs-table-bg: var(--mpa-neutral-bg); --bs-table-color: var(--mpa-ink); --bs-table-border-color: var(--mpa-hairline); --bs-table-hover-bg: var(--mpa-neutral-bg); --bs-table-hover-color: var(--mpa-ink); --bs-table-striped-bg: var(--mpa-neutral-bg); }
.table-primary   { --bs-table-bg: var(--mpa-tint);       --bs-table-color: var(--mpa-ink); --bs-table-border-color: var(--mpa-hairline); --bs-table-hover-bg: var(--mpa-tint);       --bs-table-hover-color: var(--mpa-ink); --bs-table-striped-bg: var(--mpa-tint); }

/* ── UI-CONSISTENCY-1: BADGE BRIDGE ──
   Bootstrap .badge (137 renders, 28 templates) sat beside .mpa-chip
   (1,630 renders) as a second status vocabulary: 10px rounded rectangle,
   saturated fill, white text, Bootstrap's own reds and greens. It now
   wears the chip's geometry (999px pill, same type) and the chip's
   colour philosophy (soft tint + same-hue ink) via the same semantic
   tokens, so an "Active" badge and an "Active" chip are the same object.
   Bootstrap's .bg-*/.text-bg-* utilities are !important, hence the
   !important here -- the badge-scoped selector is what keeps this from
   touching any other use of .bg-danger in the product. */
.badge:not(.mpa-chip) {
  --bs-badge-border-radius: 999px;
  --bs-badge-font-weight: 700;
  --bs-badge-padding-x: .6rem;
  --bs-badge-padding-y: .2rem;
  --bs-badge-font-size: var(--mpa-text-xs);
  letter-spacing: .02em;
  line-height: 1.35;
  vertical-align: middle;
}
.badge.bg-primary,   .badge.text-bg-primary   { background-color: var(--mpa-tint) !important;       color: var(--mpa-ink-brand) !important; }
.badge.bg-secondary, .badge.text-bg-secondary { background-color: var(--mpa-neutral-bg) !important; color: var(--mpa-neutral-ink) !important; }
.badge.bg-success,   .badge.text-bg-success   { background-color: var(--mpa-ok-bg) !important;      color: var(--mpa-ok-ink) !important; }
.badge.bg-danger,    .badge.text-bg-danger    { background-color: var(--mpa-danger-bg) !important;  color: var(--mpa-danger-ink) !important; }
.badge.bg-warning,   .badge.text-bg-warning   { background-color: var(--mpa-warn-bg) !important;    color: var(--mpa-warn-ink) !important; }
.badge.bg-info,      .badge.text-bg-info      { background-color: var(--mpa-info-bg) !important;    color: var(--mpa-info-ink) !important; }
.badge.bg-light,     .badge.text-bg-light     { background-color: var(--mpa-hairline) !important;   color: var(--mpa-muted) !important; }
.badge.bg-dark,      .badge.text-bg-dark      { background-color: var(--mpa-ink) !important;        color: #fff !important; }
/* Count pills on the dark drawer (Support unread etc.) need a SOLID fill
   to read against the deep drawer shade; they keep the semantic hue. */
.brand-nav .badge.bg-danger { background-color: var(--mpa-danger) !important; color: #fff !important; }
.brand-nav .badge.bg-primary { background-color: var(--mpa-drawer-accent) !important; color: var(--mpa-drawer-bg) !important; }

/* Form controls: brand focus ring, soft borders */
.form-control, .form-select {
  border-color: var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  color: var(--mpa-ink);
}
.form-control:focus, .form-select:focus {
  border-color: var(--mpa-action);
  box-shadow: 0 0 0 3px rgba(var(--mpa-action-rgb), .18);
}
.form-check-input:checked { background-color: var(--mpa-action); border-color: var(--mpa-action); }
.form-check-input:focus { box-shadow: 0 0 0 3px rgba(var(--mpa-action-rgb), .18); border-color: var(--mpa-action); }
.form-label { font-weight: 600; font-size: var(--mpa-text-md); }
.form-text { color: var(--mpa-muted); font-size: var(--mpa-text-sm); }
/* Help that follows a whole ROW of controls (a field + its action button),
   not one field: it is the next group, so it takes the group step -- the
   same .5rem the form grid's own g-2 puts between stacked control rows --
   rather than the label/control/help .25rem that made it hug the button
   (Staff -> Class assignments & pay, 19 Oct 2026). */
.form-text.mpa-help-after-controls { margin-top: var(--mpa-space-group); }
/* UI-CONSISTENCY-1: Bootstrap's .small is .875EM -- relative to whatever
   it sits in -- so the same "meta" class rendered at 9.75 / 10.465 /
   11.375px depending on the parent (the audit's biggest cluster of
   accidental sizes, 4,500+ text nodes at 1440). One rem token, the house
   meta size the reference pages' .mpa-row-side / .mpa-page-sub already
   use, so "text-muted small" is one size wherever it appears. */
.small, small { font-size: var(--mpa-text-sm); }

/* Default Bootstrap tabs/pills pick up the brand active state so any page
   not yet migrated to .mpa-tabs still tracks the school colour. */
.nav-pills { --bs-nav-pills-link-active-bg: var(--mpa-action); }
.nav-tabs { --bs-nav-tabs-link-active-color: var(--mpa-ink-brand); }
.nav-link { --bs-nav-link-color: var(--mpa-ink-brand); --bs-nav-link-hover-color: var(--mpa-action-hover); }

/* ============================================================
   DRAWER / SIDEBAR — the premium branded navigation surface.
   Deep derived shade (never the raw school colour), subtle
   vertical gradient so it doesn't read as a flat slab.
   ============================================================ */
.brand-nav {
  background: linear-gradient(180deg, var(--mpa-drawer-top) 0%, var(--mpa-drawer-bg) 34%);
  color: #fff;
  font-family: var(--mpa-font);
}
/* iOS installed-PWA drawer-header safe-area inset (drawer safe-area fix
   V1, 2 Sep 2026): the PAGE-header fix above (.main-content) doesn't
   reach here — the drawer is a separate offcanvas panel, opened over the
   page, so its own header needs the identical treatment or the logo/close
   sit under the status bar/Dynamic Island exactly as .main-content's
   content used to. Deliberately scoped to padding-TOP only, on the
   HEADER, not the panel: the panel (`.brand-nav`, the offcanvas itself)
   stays `top: 0` / full height so its purple background still extends
   behind the unsafe area — only the header's own content needs pushing
   down. Same calc(existing + env(...), 0px)) pattern as .main-content:
   env() is 0 in every context that doesn't need it, so this is byte-
   identical to the pre-existing 1.05rem there. The >=992px (and the
   min-width:992px/max-height:720px) overrides below replace this rule's
   `padding` shorthand wholesale with flat desktop values that carry no
   env() at all, so desktop is unaffected by design, same as the page
   header's own contract. */
.brand-nav .offcanvas-header {
  padding: calc(1.05rem + env(safe-area-inset-top, 0px)) 1.1rem .95rem;
  border-bottom: 1px solid rgba(255,255,255,.10) !important;
}
/* Close-control tap target, MOBILE ONLY (drawer safe-area fix V1, amendment
   2 Sep 2026): Rory's physical-iPhone report was two-fold — the safe-area
   overlap above, AND the × itself being awkward to press even once visible.
   Bootstrap's .btn-close is a 16px (1em) content box + 8px padding/side =
   32x32 here — under the 44x44 iOS HIG floor. box-sizing is content-box
   (Bootstrap default, unchanged), so growing PADDING grows only the
   clickable box; the glyph itself is a `background: ... center/1em` image
   drawn inside the untouched 16px content box, so it stays pixel-identical
   in size — only the invisible hit area around it grows. 14px/side ->
   16 + 14*2 = 44x44 exactly. Guarded to <992px so Desktop Density V3's
   accepted 26x26 close button (no override here — Bootstrap's own default
   padding applies there, exactly as already banked) is untouched by
   construction, same rule as the header padding-top above. */
@media (max-width: 991.98px) {
  .brand-nav .btn-close { padding: 14px; }
}
/* White chip behind the logo so ANY uploaded logo reads on the deep drawer */
.brand-nav .drawer-logo {
  display: inline-flex; align-items: center;
  background: rgba(255,255,255,.96);
  border-radius: 10px; padding: .4rem .65rem;
  text-decoration: none;
}
.brand-nav .drawer-logo img { height: 32px; width: auto; display: block; border-radius: 3px; }
.brand-nav .nav { padding: .5rem .65rem; row-gap: 2px; }
.brand-nav .nav-link {
  display: flex; align-items: center; position: relative;
  color: rgba(255,255,255,.72);
  font-size: var(--mpa-text-base); font-weight: 500;
  padding: .55rem .85rem .55rem 1rem;   /* ≥40px touch target; left room for the active accent bar */
  border-radius: var(--mpa-radius-sm);
  margin: 0;
  transition: background .14s ease, color .14s ease;
}
.brand-nav .nav-link i {
  width: 1.5rem; margin-right: .4rem; text-align: center;
  font-size: 1rem; opacity: .8;
}
/* Hover: warmed with a whisper of the pastel brand accent (white-alpha fallback) */
.brand-nav .nav-link:hover {
  color: #fff;
  background: rgba(255,255,255,.06);
  background: color-mix(in srgb, var(--mpa-drawer-accent) 9%, transparent);
}
/* Active: soft brand-tinted glass pill + pastel accent bar + tinted icon.
   White text stays >= 4.5:1 through the overlay (verified in the colour-
   safety test, which mirrors this 17% mix). */
.brand-nav .nav-link.active {
  color: #fff; font-weight: 600;
  background: rgba(255,255,255,.12);
  background: color-mix(in srgb, var(--mpa-drawer-accent) 17%, transparent);
}
.brand-nav .nav-link.active::before {
  content: ""; position: absolute; left: 6px; top: 24%; bottom: 24%;
  width: 3px; border-radius: 3px; background: var(--mpa-drawer-accent);
}
.brand-nav .nav-link.active i { opacity: 1; color: var(--mpa-drawer-accent); }

/* Bottom account area */
.brand-nav .drawer-account {
  padding: .95rem 1rem calc(.95rem + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid rgba(255,255,255,.10);
}
.brand-nav .drawer-user { display: flex; align-items: center; gap: .7rem; margin-bottom: .8rem; }
.brand-nav .drawer-avatar {
  width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0;
  display: grid; place-items: center;
  background: rgba(255,255,255,.14);
  color: #fff; font-weight: 700; font-size: var(--mpa-text-md); letter-spacing: .02em;
}
.brand-nav .drawer-name {
  color: #fff; font-size: var(--mpa-text-md); font-weight: 600; text-decoration: none;
  display: block; line-height: 1.25;
}
.brand-nav .drawer-name:hover { color: #fff; text-decoration: underline; }
.brand-nav .drawer-name i { font-size: var(--mpa-text-2xs); opacity: .6; }
.brand-nav .drawer-role {
  display: block; margin-top: .1rem;
  font-size: var(--mpa-text-xs); font-weight: 600; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
  color: rgba(255,255,255,.55);
}
.brand-nav .drawer-signout {
  width: 100%;
  background: rgba(255,255,255,.08);
  border: 1px solid rgba(255,255,255,.16);
  color: #fff; font-size: var(--mpa-text-sm); font-weight: 600;
  border-radius: var(--mpa-radius-sm); padding: .48rem;
  transition: background .14s ease;
}
.brand-nav .drawer-signout:hover { background: rgba(255,255,255,.15); color: #fff; }

/* Hamburger button (shared macro) — small white card button */
.mpa-menu-btn {
  background: var(--mpa-surface); border: 1px solid var(--mpa-line); color: var(--mpa-ink);
  width: 2.5rem; height: 2.5rem; border-radius: 12px;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: var(--mpa-shadow);
  transition: border-color .15s, color .15s;
}
.mpa-menu-btn:hover { border-color: var(--mpa-action); color: var(--mpa-ink-brand); background: var(--mpa-surface); }

/* ============================================================
   FOUNDATION COMPONENTS (Stage A: defined centrally, applied to
   pages in Stage B — matches the Dashboard/Reports prototypes)
   ============================================================ */

/* Page header */
.mpa-page-head { display: flex; align-items: center; gap: .9rem; flex-wrap: wrap; margin-bottom: 1.25rem; }
/* §5 (29 Aug 2026, FINAL FINAL acceptance corrective) — Staff LIST page:
   same root cause as .mpa-detail-head's hamburger-stranding bug earlier
   in this build (see .mpa-detail-head-row's own comment) — .mpa-page-head
   is flex-wrap:wrap with no min-width:0 on its title block, so a WIDE
   block gets evicted to its own line, stranding the hamburger above it.
   Students/Invoices/Classes never hit this because their title block is
   a bare <h2> with no subtitle, narrow enough to never trigger it; Staff
   adds a full explanatory sentence beneath its title, which is wide
   enough to. Same fix, generic name (list-page headers use
   align-items:center, detail-page headers use flex-start, so this is a
   separate class from .mpa-detail-head-row rather than forcing one
   alignment choice on both). */
.mpa-head-row { display: flex; align-items: center; gap: .9rem; flex: 1 1 auto; min-width: 0; }
/* Matches the app-standard page title EXACTLY — plain Bootstrap
   `<h2 class="mb-0 fw-bold">` (h2 default 2rem + fw-bold 700), the pattern
   every un-migrated page (Classes, Students, Staff, Settings, …) still
   uses. .mpa-page-title exists so redesigned pages reach this size through
   one shared token instead of re-declaring Bootstrap's own numbers —
   drifting from them (as it did at 1.5rem/800) defeats that purpose. */
/* Sized with Bootstrap's OWN responsive h2 formula, not a fixed 2rem —
   un-migrated pages use plain <h2> (RFS: calc below 1200px, 2rem above),
   and a fixed value here made migrated pages' titles visibly LARGER on
   mobile (S5 round 7 / standing rule: all page titles the same size at
   every width, every page). */
.mpa-page-title { font-size: calc(1.325rem + .9vw); font-weight: 700; margin: 0; line-height: 1.2; }
@media (min-width: 1200px) { .mpa-page-title { font-size: 2rem; } }
.mpa-page-sub { color: var(--mpa-muted); font-size: var(--mpa-text-sm); font-weight: 500; margin-top: .1rem; }
/* Header Standardisation Amendment (4 Sep 2026, Rory's post-review pass)
   -- .mpa-head-secondary: a guaranteed full-width SECOND LINE within
   .mpa-page-head's flex-wrap row, for a page subtitle, a term's
   date-range, or any other explanatory text that must sit BELOW the
   primary row's controls without ever affecting their OWN vertical
   alignment.

   Root cause this fixes (measured before/after, see
   HANDOFF_dashboard_attendance_tenant_logo_v1.md's amendment section):
   a subtitle/date-range previously lived NESTED inside the same flex
   item as the title/term-control it belongs to (e.g. <div><h2>Title
   </h2><div class="mpa-page-sub">...</div></div>), making that ONE flex
   item taller than its siblings (hamburger, logo, term control) --
   align-items:center then centred every sibling against THAT item's
   full (subtitle-inclusive) height, landing them visibly below the
   title's own text line -- exactly Rory's "logo sits lower than the
   title" report, reproduced on Dashboard/Class Plans/Staff/Curriculum/
   Timetable/Reports before this fix.

   Fix: promote the subtitle/date-range to a DIRECT sibling of the
   primary-row items (title, term control, logo, ...) carrying this
   class, instead of nesting it inside any one of them. Below 1280px,
   .mpa-page-head is already a grid (the existing canonical mobile
   header, extended to 1279.98px by the tablet-squeeze-guard rule below)
   whose own catch-all already turns "any child besides .mpa-menu-btn/
   .mpa-head-row/.mpa-home-logo" into its own full-width row -- this
   class only needs to do anything at >=1280px, where .mpa-page-head is
   still a plain flex row; flex-basis has no effect once display:grid
   takes over, so one unconditional rule is safe at every width. */
.mpa-head-secondary { flex-basis: 100%; width: 100%; order: 5; margin-top: .1rem; min-width: 0; }
/* Title + inline greeting companion — sit on ONE baseline-aligned line
   (e.g. "Dashboard  Good evening, Rory", plain spacing, no separator).
   .mpa-page-title is untouched here (same size/weight as every other
   page); the greeting is deliberately lighter/smaller so it reads as a
   warm companion, not a second headline — wraps below the title on
   narrow screens rather than forcing a squeeze. */
.mpa-title-row { display: flex; align-items: baseline; flex-wrap: wrap; column-gap: .9rem; row-gap: .15rem; min-width: 0; max-width: 100%; }
/* Guards against silent clipping: mobile layouts use overflow-x:clip at the
   page level (not scroll), so a nowrap element wider than its flex row simply
   vanishes off the edge with no scrollbar to reveal it. min-width:0 lets this
   flex child actually shrink (flex items default to min-width:auto, i.e. their
   own content width, which defeats shrinking); overflow/text-overflow then
   ellipsis the excess instead of letting it disappear. Text that already fits
   is unaffected. */
.mpa-greeting-inline { color: var(--mpa-ink); font-size: var(--mpa-text-lg); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; min-width: 0; }
/* Opt-out for pages whose companion string IS the identity (parent
   invoice: "name — term · invoice N · date") — final visual corrective
   B1, 25 Aug 2026: there, truncation loses real information, so this
   modifier wraps to a second line instead. The ellipsis default above
   stays for every other page (a greeting can safely truncate; an
   invoice identity can't). */
.mpa-greeting-inline.mpa-greeting-wrap { white-space: normal; overflow: visible; text-overflow: clip; }

/* One-Click Tenant-Logo Home Navigation V1 (4 Sep 2026) — tenant logo,
   far right of .mpa-page-head, doubling as a one-click Dashboard link
   (partials/_sidebar.html's page_home_logo() macro). Geometry derived
   from the REAL existing header, not invented: image height 28px mobile
   (matches the mobile .mpa-page-title's 1.6rem/25.6px line reasonably,
   with a touch of breathing room) / 32px desktop (comfortable against
   the desktop title's larger line-height, and close to .mpa-menu-btn's
   own 2.5rem/40px box) — the "~28-32px" the brief asked for, chosen
   from the real .mpa-page-title/.mpa-menu-btn sizes already in this
   file rather than picked arbitrarily. The VISIBLE box never grows
   beyond the image + a little padding — the 44x44px touch target the
   brief also asks for is a separate, invisible ::before hit-area (see
   below) so it can never inflate .mpa-page-head's row height. max-width
   caps a long wordmark well short of ever crowding the title (checked
   against the real DEV school's own uploaded logo plus synthetic wide-
   wordmark/square/no-logo cases during QA — see
   HANDOFF_dashboard_attendance_tenant_logo_v1.md for the actual
   whitespace/aspect-ratio findings on the school's own uploaded file). */
.mpa-home-logo {
  position: relative; /* anchors the ::before hit-target below */
  display: inline-flex; align-items: center; justify-content: flex-end;
  padding: .2rem .05rem;
  border-radius: var(--mpa-radius-sm, 10px);
  flex-shrink: 0;
  text-decoration: none;
}
/* Enlarges the CLICKABLE/tappable area to the ~44x44px the brief asks
   for (WCAG 2.5.5) without the visible box (or .mpa-page-head's grid
   row) growing to match -- position:absolute removes this from layout
   flow entirely, so the header's height still comes only from the 28px/
   32px image + small padding, never from the touch target. A click/tap
   anywhere in this invisible box IS a click on the real <a> (pseudo-
   element hits are dispatched to their host element), so this is a
   genuine bigger target, not a cosmetic one. */
.mpa-home-logo::before {
  content: "";
  position: absolute;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  width: 44px; height: 44px;
}
.mpa-home-logo img {
  display: block; height: 28px; width: auto; max-width: 84px;
  object-fit: contain; object-position: right center;
  position: relative; /* paints above the ::before hit-target */
}
.mpa-home-logo:hover { background: var(--mpa-tint); }
.mpa-home-logo:focus-visible {
  outline: 2px solid var(--mpa-action); outline-offset: 2px;
  border-radius: var(--mpa-radius-sm, 10px);
}
/* Desktop: sit right after whatever already carries the page's own
   right-pinned control (ms-auto/ms-lg-auto) with a small fixed gap —
   :has() means no per-page margin decision is ever needed here; when a
   page has NO such control, the logo takes the auto-margin itself and
   is the only thing pushed to the far right. extra_class (macro param)
   is the escape hatch for the one header shape that isn't a plain flex
   row (see the macro's own docstring). */
.mpa-page-head:has(.ms-auto, .ms-lg-auto) > .mpa-home-logo { margin-left: .6rem; }
.mpa-page-head:not(:has(.ms-auto, .ms-lg-auto)) > .mpa-home-logo { margin-left: auto; }
@media (min-width: 768px) {
  .mpa-home-logo img { height: 32px; max-width: 140px; }
}

/* Term-status block — a term pill (name + status dot) with its date range
   underneath, right-aligned. Shared by Dashboard and Reports so both
   pages' top-right corner reads identically. */
.mpa-term-block { display: flex; flex-direction: column; align-items: flex-end; gap: .3rem; }
/* Teacher Dashboard polish (7 Aug 2026, Rory): the date range centred
   under the pill instead of right-aligned — a modifier, not a change to
   .mpa-term-block itself, so admin Dashboard/Reports keep the shared
   right-aligned look untouched. */
.mpa-term-block-center { align-items: center; }
.mpa-pill {
  display: inline-flex; align-items: center; gap: .5rem;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line); border-radius: 999px;
  padding: .45rem 1rem; font-size: var(--mpa-text-sm); font-weight: 600;
  box-shadow: var(--mpa-shadow); white-space: nowrap;
}
.mpa-pill .dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--mpa-ok); flex-shrink: 0; }
.mpa-pill.warn .dot { background: var(--mpa-warn); }

/* STANDING RULE (25 July 2026, §1c): on mobile, term selectors and other
   page-head controls sit LEFT-aligned — under the title/actions, not
   pinned to the right edge — and never truncate: the full term name
   stays readable (wrap, don't clip). The !importants are needed to beat
   Bootstrap's ms-auto / text-end utilities, which are !important
   themselves. Un-migrated page headers (bespoke markup) get the same
   treatment per-template: ms-lg-auto + w-auto/max-width:100% selects. */
@media (max-width: 991.98px) {
  /* Page-level guarantee (S5 round 8): ZERO horizontal scroll on mobile,
     app-wide. clip, not hidden — it forbids sideways scroll AND the
     emulated-touch elastic overscroll (the "dark gap on the right" in
     DevTools device mode), creates no scroll container, and — unlike
     hidden — doesn't break position:sticky descendants. Measured before
     adding: both timetable and attendance already at 0px genuine
     overflow, so this is a guard rail, not a mask. */
  html, body { overflow-x: clip; }
  .mpa-page-head .mpa-term-block { margin-left: 0 !important; align-items: flex-start; }
  /* Date-range line indents to the SELECT'S TEXT (border + padding of
     .form-select-sm), so select text and date range align as a pair. */
  .mpa-term-block .mpa-page-sub { text-align: left !important; padding-left: calc(.5rem + 1px); }
  .mpa-pill { white-space: normal; }
}

/* Section card */
.mpa-card {
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius); box-shadow: var(--mpa-shadow); overflow: hidden;
}
.mpa-card-head { display: flex; align-items: center; gap: .6rem; padding: 1rem 1.25rem .75rem; }
/* Rory UX follow-up (10 Sep 2026): every Needs Attention card whose body
   is a .mpa-rows list gets its "divider beneath the header" for free --
   .mpa-row's own border-top happens to land exactly on that boundary.
   A single-status card (QuickBooks connection, Current term has ended)
   has no rows at all, so it never got one -- not a deliberate style
   choice, just an accident of two different body shapes. Opt-in
   modifier, same hairline/weight .mpa-row already uses, so a
   single-status card can match the list cards' header exactly without
   changing every other single-status card's look (QBO's own connection
   card is intentionally left as-is here -- add the class there too if
   it should ever match). */
.mpa-card-head-divided { border-bottom: 1px solid var(--mpa-hairline); }
.mpa-card-title {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
  color: var(--mpa-muted);
}
.mpa-card-title i { font-size: var(--mpa-text-base); color: var(--mpa-ink-brand); }
.mpa-card.warn-card .mpa-card-title i { color: var(--mpa-warn); }
.mpa-card.ok-card .mpa-card-title i { color: var(--mpa-ok); }
.mpa-card.danger-card .mpa-card-title i { color: var(--mpa-danger); }
.mpa-card-body { padding: .35rem 1.25rem 1.1rem; }
.mpa-link { margin-left: auto; font-size: var(--mpa-text-sm); font-weight: 700; text-decoration: none; color: var(--mpa-ink-brand); white-space: nowrap; }
.mpa-link:hover { text-decoration: underline; color: var(--mpa-ink-brand); }

/* Icon tile (soft wash of the raw school hue, brand-ink glyph) */
.mpa-icon-tile {
  width: 48px; height: 48px; border-radius: 14px; flex-shrink: 0;
  display: grid; place-items: center; font-size: 1.3rem;
  background: var(--mpa-tint); color: var(--mpa-ink-brand);
}
.mpa-icon-tile.sm { width: 30px; height: 30px; border-radius: var(--mpa-radius-sm); font-size: var(--mpa-text-base); }

/* KPI card */
.mpa-kpi {
  display: flex; align-items: center; gap: 1rem; height: 100%;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius);
  padding: 1.2rem 1.25rem; text-decoration: none; color: var(--mpa-ink);
  box-shadow: var(--mpa-shadow);
}
a.mpa-kpi { transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; }
a.mpa-kpi:hover {
  transform: translateY(-2px); color: var(--mpa-ink);
  border-color: var(--mpa-action); box-shadow: var(--mpa-shadow-lift);
}
.mpa-kpi-value { font-size: var(--mpa-text-2xl); font-weight: 800; line-height: 1.05; letter-spacing: -.03em; }
.mpa-kpi-value.money { font-size: var(--mpa-text-xl); letter-spacing: -.02em; line-height: 1.1; }
.mpa-kpi-value.negative { color: var(--mpa-danger); }
.mpa-kpi-label {
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
  color: var(--mpa-muted); margin-top: .3rem;
}
.mpa-kpi-note { font-size: var(--mpa-text-xs); color: var(--mpa-muted); margin-top: .15rem; }
/* Label + qualifier sharing one row (Staff Costs' Estimated Profit, §13C)
   -- flex-wrap so a narrow phone still falls back to the original two
   stacked lines with no extra markup per breakpoint. */
.mpa-kpi-label-row { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: .35em; }
.mpa-kpi-label-row .mpa-kpi-label { margin-top: .3rem; }
.mpa-kpi-label-row .mpa-kpi-note-inline { margin-top: .3rem; }
@media (min-width: 768px) {
  .mpa-kpi-label-row .mpa-kpi-note-inline::before { content: "\00b7\0020"; }
}
.mpa-kpi-arrow {
  margin-left: auto; color: var(--mpa-ink-brand); opacity: 0; transform: translateX(-5px);
  transition: opacity .16s ease, transform .16s ease; font-size: var(--mpa-text-lg);
}
a.mpa-kpi:hover .mpa-kpi-arrow { opacity: .85; transform: none; }

/* New Joiners mobile record (PWA Responsive UI Consistency Polish V1,
   §13G): name + real join date share the primary line (date right-
   aligned, name allowed to shrink first via min-width:0 on the flex
   item), class on its own secondary line. flex-wrap lets a missing-date
   label ("Date not recorded") fall to its own line instead of overflowing
   at ~375px -- no separate breakpoint/JS needed, the flex row just wraps
   when the two pieces of text don't both fit. */
.mpa-newjoiner-row { padding: .55rem 1.25rem; border-bottom: 1px solid var(--mpa-line); }
.mpa-newjoiner-row:last-child { border-bottom: none; }
.mpa-newjoiner-row-top {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
  column-gap: .75rem; row-gap: .1rem;
}
.mpa-newjoiner-row-top .mpa-link { min-width: 0; overflow-wrap: anywhere; }
.mpa-newjoiner-date { font-size: var(--mpa-text-sm); font-weight: 600; color: var(--mpa-muted); white-space: nowrap; flex-shrink: 0; }
.mpa-newjoiner-row-sub { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin-top: .1rem; overflow-wrap: anywhere; }

/* Filter bar */
.mpa-filter-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem 1.1rem;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius); box-shadow: var(--mpa-shadow);
  padding: .7rem 1rem; margin-bottom: var(--mpa-space-block);
}
.mpa-filter-bar .form-label {
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
  color: var(--mpa-muted); margin: 0; white-space: nowrap;
}
.mpa-filter-bar form { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.mpa-filter-bar .form-select-sm, .mpa-filter-bar .form-control-sm { font-size: var(--mpa-text-sm); font-weight: 500; }
/* Final Forensic Corrective Pass (4 Sep 2026, §5) -- ONE field surface for
   every control that sits in a filter bar. Root cause of the grey-vs-white
   mix (measured: combo inputs rgb(247,246,250) beside white selects on
   Attendance/Invoices): Bootstrap's .form-control paints
   var(--bs-body-bg), which this app points at --mpa-bg (the lavender page
   canvas), while .mpa-select explicitly paints --mpa-surface (white). The
   text inputs were never DELIBERATELY grey -- .mpa-field (the design
   system's own input class) already declares --mpa-surface. Aligned here
   at the filter-bar contract (and for the shared combo component wherever
   it appears), plus one shared text inset so an input's text and a
   select's text start at the same x. */
.mpa-filter-bar .form-control,
.mpa-combo .form-control { background-color: var(--mpa-surface); }
.mpa-filter-bar .form-control-sm,
.mpa-filter-bar .form-select-sm,
.mpa-filter-bar .mpa-select { padding-left: .65rem; }
/* OPS-1 FINAL MICRO-POLISH (Part 1) — ONE shared geometry contract for
   every control that can sit in a filter bar (the timetable's Teacher/
   Venue/Student combo inputs, the Flag select, Clear filters). Two
   independent root causes were making Flag and Clear filters look
   different from the combo inputs next to them, both fixed here rather
   than with per-page magic numbers:
   (1) .mpa-select is a general-purpose component used elsewhere at a
       deliberately roomy 42px min-height/padding — correct for its
       other callers, wrong inside a compact filter row. This selector
       (0,2,0) outranks the bare .mpa-select rule (0,1,0), so it wins
       without needing !important.
   (2) Bootstrap's .btn-sm and .form-control-sm/.form-select-sm are
       nominally matched, but the font-size override two lines up only
       ever touched form-control/form-select, leaving Clear filters at
       Bootstrap's own .875rem instead of this bar's .82rem — a small
       but real line-height/height difference.
   inline-flex + a fixed height on <select>/<input>/<a class="btn">
   alike guarantees identical vertical centring regardless of each
   element's own native line-height quirks. Width is deliberately
   untouched — combo inputs and Flag keep their own min-width/style,
   Clear filters keeps sizing to its label. */
.mpa-filter-bar .form-select-sm,
.mpa-filter-bar .form-control-sm,
.mpa-filter-bar .mpa-select,
.mpa-filter-bar .btn-sm {
  height: 33px;
  min-height: 33px;
  padding-top: 0;
  padding-bottom: 0;
  display: inline-flex;
  align-items: center;
  border-radius: var(--mpa-radius-sm);
  font-size: var(--mpa-text-sm);
  line-height: 1;
}
/* Label+control pair that must wrap together as one unit (used when the
   whole filter bar is a single htmx live-filter form rather than several
   small per-field forms, e.g. Students/Staff/Classes list filters). */
.mpa-filter-bar .mpa-filter-field { display: flex; align-items: center; gap: .45rem; }
/* Fill variant (Invoices, 3 Aug 2026): every field shares the row width
   equally and the controls stretch, so the bar reads as one aligned row
   edge-to-edge instead of a raggle of fixed-width boxes. The trailing
   Clear button keeps its natural width. */
.mpa-filter-bar.mpa-filter-fill .mpa-filter-field { flex: 1 1 0; min-width: 10rem; }
/* Equal labels -> equal boxes: with flex-shared fields, any label-width
   difference (SEARCH vs SORT) would come out of its control's width. */
.mpa-filter-bar.mpa-filter-fill .mpa-filter-field .form-label { width: 3.4rem; flex-shrink: 0; }
.mpa-filter-bar.mpa-filter-fill .mpa-filter-field .form-select,
.mpa-filter-bar.mpa-filter-fill .mpa-filter-field .form-control,
.mpa-filter-bar.mpa-filter-fill .mpa-filter-field .mpa-combo { width: 100% !important; min-width: 0; }
/* Flex variant (Final Forensic Corrective Pass, 4 Sep 2026, §5 -- Attendance):
   unlike .mpa-filter-fill (equal shares, fixed label column), every field
   keeps its own label width and SHRINKS from its preferred width down to a
   readable floor so the whole bar fits ONE row wherever the viewport
   physically allows (1366/1440 measured), and wraps deliberately below
   that. The controls' inline width= (the combo macro's own style) becomes
   the preferred size, not a hard size. */
.mpa-filter-bar.mpa-filter-flex .mpa-filter-field { flex: 1 1 8.5rem; min-width: 8.5rem; }
.mpa-filter-bar.mpa-filter-flex .mpa-filter-field .form-select,
.mpa-filter-bar.mpa-filter-flex .mpa-filter-field .form-control,
.mpa-filter-bar.mpa-filter-flex .mpa-filter-field .mpa-combo { width: 100% !important; min-width: 0; flex: 1 1 auto; }
.mpa-filter-bar.mpa-filter-flex > .btn { flex: 0 0 auto; }

/* Row-action dropdown (kebab menu) — used across every list page. Bootstrap
   mechanism/JS untouched; this is a pure visual polish so the menu matches
   the card/button roundness and soft shadow language everywhere else. */
.dropdown-menu {
  border: 1px solid var(--mpa-line); border-radius: 12px;
  box-shadow: 0 4px 12px rgba(32,25,60,.06), 0 12px 32px -12px rgba(32,25,60,.18);
  padding: .4rem; font-size: var(--mpa-text-md);
}
.dropdown-item { border-radius: 8px; padding: .45rem .65rem; }
.dropdown-item:hover, .dropdown-item:focus { background: var(--mpa-hairline); }
.dropdown-item.text-danger:hover, .dropdown-item.text-danger:focus { background: var(--mpa-danger-bg); }
.dropdown-item.text-success:hover, .dropdown-item.text-success:focus { background: var(--mpa-ok-bg); }
.dropdown-divider { border-color: var(--mpa-hairline); margin: .35rem 0; }

/* Segmented tabs — restyles Bootstrap .nav-tabs/.nav-pills when wrapped */
.mpa-tabs.nav-tabs, .mpa-tabs.nav-pills, .mpa-tabs {
  display: inline-flex; flex-wrap: wrap; gap: 2px;
  background: var(--mpa-track); border: none; border-radius: 12px; padding: 4px;
}
.mpa-tabs .nav-link {
  border: none; border-radius: 9px; margin: 0;
  font-size: var(--mpa-text-sm); font-weight: 600; color: var(--mpa-muted);
  padding: .45rem .95rem; transition: color .12s, background .12s;
}
.mpa-tabs .nav-link:hover { color: var(--mpa-ink); border: none; isolation: auto; }
.mpa-tabs .nav-link.active {
  background: var(--mpa-surface); color: var(--mpa-ink-brand);
  box-shadow: 0 1px 3px rgba(32,25,60,.12);
}

/* ── Two-tier settings navigation (V1A acceptance corrective Part I,
   25 Aug 2026). V1A put .mpa-tabs on BOTH the group row and the page row,
   which made the two tiers read as the same kind of thing — Rory couldn't
   tell the major area from the active page at a glance.
   Tier 1 (.mpa-tabs-groups): the active GROUP is a filled brand pill —
   unmissable, one per row.
   Tier 2 (.mpa-subtabs): a subordinate underline row sitting on a shared
   hairline, the active PAGE marked by a solid brand underline + brand
   ink — visually "inside" the group above it rather than a second
   competing pill row. */
.mpa-tabs.mpa-tabs-groups .nav-link.active {
  background: var(--mpa-action); color: #fff;
  box-shadow: 0 1px 3px rgba(32,25,60,.18);
}
.mpa-subtabs {
  display: flex; flex-wrap: wrap; gap: 0 .25rem;
  border-bottom: 1px solid var(--mpa-line);
}
.mpa-subtabs .nav-link {
  border: none; border-bottom: 2px solid transparent; border-radius: 0;
  margin-bottom: -1px;                 /* underline sits ON the hairline */
  font-size: var(--mpa-text-sm); font-weight: 600; color: var(--mpa-muted);
  padding: .5rem .8rem; transition: color .12s, border-color .12s;
}
.mpa-subtabs .nav-link:hover { color: var(--mpa-ink); }
.mpa-subtabs .nav-link.active {
  color: var(--mpa-ink-brand); font-weight: 700;
  border-bottom-color: var(--mpa-action);
}
@media (max-width: 575.98px) {
  /* Deliberate 2-column grid at phone width — a 7-item row used to wrap
     irregularly (Disciplines dangling next to Event Costs); a grid gives
     aligned columns and full-width tap targets, and nothing scrolls out
     of reach. The hairline moves to each cell so the grid still reads as
     one underline family. */
  .mpa-subtabs { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border-bottom: none; }
  .mpa-subtabs .nav-item { display: contents; }
  .mpa-subtabs .nav-link { border-bottom: 2px solid var(--mpa-line); margin-bottom: 0; }
  .mpa-subtabs .nav-link.active { border-bottom-color: var(--mpa-action); }
}

/* Modern table — taller rows, hairline separators, quiet headers.
   Header row is STICKY by default (position:sticky, top:0) so it stays
   visible while scrolling a long list — sensible for every mpa-table,
   not just Students, so it's the base rule rather than a modifier.
   Background must be SOLID (not the transparent look a static header
   could get away with) or scrolled-past rows would show through the
   sticky header text.

   VIEWPORT-LEVEL sticky, not a bounded inner scroll panel (reverted 9 July
   2026 — the panel/60vh approach worked but felt cramped and wasted
   space). Two ancestors were independently defeating sticky, both cleared
   below so it escapes all the way to the real page scroll:
     1. Bootstrap's .table-responsive sets overflow-x:auto but never sets
        overflow-y — per the CSS Overflow spec, when one axis is non-
        visible and the other is left visible, the browser forces the
        other axis to compute as auto too, making .table-responsive (not
        the page) the nearest scrolling ancestor / sticky containing block.
     2. .mpa-card's own overflow:hidden (for its rounded corners) does the
        exact same thing one level further out — ANY non-visible overflow
        value (hidden included) creates a scroll container for sticky's
        purposes, even though .mpa-card never actually scrolls.
   Both are neutralised to overflow:visible, scoped via :has() to ONLY
   cards/wrappers that contain an .mpa-table, so un-migrated pages' plain
   cards/tables (and every non-table .mpa-card — KPI cards, insight
   cards, the money card, etc.) are completely untouched. Trade-off,
   accepted deliberately: this also removes horizontal-scroll capability
   on these tables — fine on desktop widths where the fixed-percentage
   columns already fit; narrow/mobile handling for these tables is its
   own future responsive pass (§9.7 backlog), not solved here. Corner-
   clipping risk from removing .mpa-card's overflow:hidden is low: the
   header/row backgrounds are the same white/near-white as the card
   itself, so nothing visibly pokes past the rounded corner even
   unclipped; the kebab dropdown already escapes any ancestor clipping
   via its own popper strategy:fixed, so it's unaffected either way.

   ── §9.7 RESOLVED, 3 Aug 2026 (Rory ruling) ──────────────────────────
   The deferred mobile pass is now done, and the two requirements turn
   out not to conflict — they just wanted different breakpoints.

   Sticky headers and horizontal scrolling are mutually exclusive by
   construction (scrolling REQUIRES the non-visible overflow that breaks
   sticky), so this is a genuine either/or per width, not a bug to fix:

     >= 768px  overflow stays visible -> sticky headers work, and every
               .mpa-table already fits, so nothing is clipped anyway.
     <  768px  overflow-x returns to auto -> the table scrolls and ALL
               columns become reachable. Sticky is knowingly given up
               here; a sticky header is worth little on a phone, whereas
               silently amputating half the columns (Invoices was losing
               47% of its width, Students 18%, Staff 7%) is a real
               failure — you cannot read a total you cannot reach.

   Applies to every .mpa-table page at once (Invoices, Students, Staff,
   Classes, Terms…) rather than being patched per page, because the
   defect was never page-specific — it was this one override. */
.table-responsive:has(.mpa-table), .mpa-card:has(.mpa-table) { overflow: visible; }
/* §4 (29 Aug 2026, FINAL FINAL acceptance corrective): same fix, same
   reason, for Class Detail's Pupils This Term sticky column header.
   .mpa-card's own overflow:hidden (rounded-corner clipping) turns out to
   make a BAD sticky containing block when the card never scrolls
   internally (auto height, content always fits) -- verified live that a
   position:sticky descendant just tracked the page scroll linearly past
   top:0 instead of clamping there. Opting this one card out of
   overflow:hidden the same way the table cards already do lets the
   sticky header bind to the real page/viewport scroll instead, which
   does clamp correctly. */
.mpa-card:has(.mpa-pupils-col-header) { overflow: visible; }

/* UI-CONSISTENCY-1B (22 Sep 2026): the scroll-not-clip contract now runs
   up to 991.98px, not 767.98px. The 3 Aug ruling assumed every .mpa-table
   already fits from 768px up; the section-P sweep measured Attendance,
   Time off, Class Interest and the Support AI list clipping 90-215
   elements (Roll Call / Withdraw / Enrol buttons off-screen) at exactly
   768px. A tablet gives up the sticky header for the same reason a phone
   does: you cannot press a button you cannot reach. >= 992px unchanged. */
@media (max-width: 991.98px) {
  /* Give the scroll container back below the desktop breakpoint. The CARD
     stays overflow:visible — only the inner wrapper scrolls, so the card's
     rounded corners and any escaping dropdown are unaffected. */
  .table-responsive:has(.mpa-table) {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* Let CONTENT size the columns once we're scrolling.
     Students/Staff/Classes set table-layout:fixed with percentage widths,
     which is right on desktop (stops Name stretching and shoving the rest
     apart) but actively harmful at phone width: percentages of ~310px are
     narrower than the words in them, so cells overlap and text collides
     rather than merely being tight — measured 1191px of content forced
     into 544px on Students. Since the wrapper now scrolls, there is no
     reason to compress at all: size to content and let the swipe reach it.

     !important is required and deliberate: table-layout:fixed is set as an
     INLINE style attribute in those templates, which a plain rule can't
     beat. Scoped to phone widths and to .mpa-table inside a scrolling
     wrapper, so desktop's fixed layout is untouched.

     The floor is a fixed 40rem, NOT max-content. max-content was tried and
     rejected: forbidding all wrapping blew Classes out to 2828px (2.5k of
     sideways swiping for one table) and Students to 1700px. Under
     table-layout:auto the cells WRAP rather than overlap, so a modest
     floor gets legible columns without turning every list into a
     marathon. 40rem ≈ 640px: comfortably past the point where columns
     collide, still only about one screen-width of swipe on a phone.

     min-width (not width) so a table narrower than the floor still just
     fills the card. Nothing is truncated — text wraps as the standing
     wrap-don't-truncate rule requires, and the scroll covers the rest. */
  .table-responsive:has(.mpa-table) > .mpa-table.table {
    table-layout: auto !important;
    min-width: 40rem;
  }
  /* Narrow-table opt-out (Rory, 6 Aug 2026 — Settings/Invoicing late-fee
     rules): the 40rem floor above is tuned for wide, many-column tables
     (Students/Staff/Invoices) where a smaller floor would collide columns.
     A short table (few columns, short cell content) doesn't need that
     floor at all -- forcing one just adds a pointless horizontal scroll
     for content that already fits. .mpa-table-narrow drops the floor so
     table-layout:auto sizes purely to content instead. */
  .table-responsive:has(.mpa-table.mpa-table-narrow) > .mpa-table.table {
    min-width: 0;
  }
  /* Real-device Reports amendment (forensic fix): the SAME 640px-floor
     defect as §I above, on every .mpa-table-stack table (Revenue by
     class, Attendance by-class, Staff Costs per-teacher, Profitability
     Full Detail). PROVEN by measurement at 390px: the table-stack rule
     (below, <992px) sets width:100%, but THIS floor -- <768px, so active
     on every real iPhone width -- forced min-width:40rem regardless,
     leaving a 640px table sitting inside a 324px card. The card-stack
     rows were rendering correctly; they were just 640px wide inside a
     scrollable-but-visually-silent .table-responsive wrapper, so most of
     every row's value was scrolled off-screen -- exactly Rory's "labels
     visible, values missing" report. A table-stack row is a self-sizing
     label:value card, not a wide table needing a floor to stop columns
     colliding -- it never needs one, at any content length. */
  .table-responsive:has(.mpa-table.mpa-table-stack) > .mpa-table.table {
    min-width: 0;
  }

  /* ── Frozen first column while the table scrolls sideways (Rory, 3 Aug
     2026) — the name stays put as the money/status columns slide under
     it. Component-level: every .mpa-table gets it with no template work.
     tfoot is EXCLUDED on purpose: Invoices' footer summary cell spans 5
     columns, and pinning a colspan cell would drag the whole footer row
     over the total as you scroll. Solid surface background is what makes
     the freeze legible — a transparent cell would just smear the columns
     underneath it. (Sticky-left inside the wrapper composes fine with the
     header's own sticky-top: different axes, same scroll container.) */
  .table-responsive:has(.mpa-table) .mpa-table.table thead th:first-child,
  .table-responsive:has(.mpa-table) .mpa-table.table tbody td:first-child {
    position: sticky; left: 0; z-index: 2;
    background: var(--mpa-surface);
  }
  .table-responsive:has(.mpa-table) .mpa-table.table thead th:first-child { z-index: 4; }
  /* Tables whose column 1 is the bulk-select checkbox (Invoices) freeze
     column 2 as well, so the NAME is what stays visible — the checkbox
     alone would be a useless anchor. left = measured col-1 width. */
  /* .table added for SPECIFICITY, not selection: the frozen header must
     beat "thead.table-light th { background: transparent }" (later in
     this file) on weight — as a tie it lost on document order, and the
     frozen Student header went see-through (caught by Rory, 4 Aug 2026,
     Invoices vs Staff comparison). Same hardening on the z-index rule
     below so the pair can't drift apart. */
  .mpa-table.table.mpa-freeze-2 thead th:nth-child(2),
  .mpa-table.table.mpa-freeze-2 tbody td:nth-child(2) {
    /* re-measured 5 Aug 2026 (Rory item 1): the checkbox column's own
       right padding was removed to close the gap to Student, narrowing
       its real mobile width from ~42px to ~30px -- re-measured in-browser,
       not guessed. */
    position: sticky; left: var(--mpa-freeze-col1, 30px); z-index: 2;
    background: var(--mpa-surface);
  }
  .mpa-table.table.mpa-freeze-2 thead th:nth-child(2) { z-index: 4; }

  /* ── Frozen-column width cap (V1A acceptance corrective Part E, 25 Aug
     2026) — the frozen first column is the right anchor, but on tables
     whose first cell is a LONG name (Class Plans' class names), an
     uncapped anchor froze most of a 360px screen and left a sliver for
     the scrolling columns. .mpa-freeze-cap bounds the anchor to under
     half the viewport and lets its text wrap (the standing
     wrap-don't-truncate rule) instead of widening. Opt-in modifier, not
     a base-rule change: Students/Staff/Invoices have short first cells
     and keep their measured behaviour untouched. */
  .table-responsive:has(.mpa-table.mpa-freeze-cap) .mpa-table.table thead th:first-child,
  .table-responsive:has(.mpa-table.mpa-freeze-cap) .mpa-table.table tbody td:first-child {
    max-width: 45vw;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  /* ── Filter bars stack into aligned label+control rows: fixed-width
     labels give one shared left edge for every control, and the controls
     all stretch to the same right edge. The !important is load-bearing —
     Students/Staff size their selects with inline width styles, which
     only an !important declaration can override. ── */
  .mpa-filter-bar .mpa-filter-field { width: 100%; }
  /* The fill variant's flex-basis:0 outranks the width above (higher
     specificity), which left the last field sharing its row with the
     Clear button. Full basis stacks every field; Clear then wraps to its
     own row, right-aligned by its ms-auto — exactly the Students look. */
  .mpa-filter-bar.mpa-filter-fill .mpa-filter-field { flex-basis: 100%; }
  /* Same reasoning applies to the flex variant (Family Key V1 final
     polish, 10 Sep 2026, item E): its desktop flex-basis (8.5rem) is a
     shrink FLOOR, not a mobile target -- without this override it would
     let two narrow fields share one row unpredictably at phone width
     instead of each stacking to its own full-width row like every other
     filter bar. Additive only: activates below 767.98px, touches no
     other page's desktop layout (currently only Attendance uses
     .mpa-filter-flex; Students adopts it in item E below). */
  .mpa-filter-bar.mpa-filter-flex .mpa-filter-field { flex-basis: 100%; }
  .mpa-filter-bar .mpa-filter-field .form-label { width: 5.2rem; flex-shrink: 0; }
  .mpa-filter-bar .mpa-filter-field .form-select,
  .mpa-filter-bar .mpa-filter-field .form-control,
  .mpa-filter-bar .mpa-filter-field .mpa-combo {
    width: 100% !important; flex: 1 1 auto; min-width: 0;
  }
  /* INVOICE-MOBILE-POLISH-1 (19 Sep 2026): the 5.2rem fixed label width
     two rules up exists to align MULTIPLE stacked label+control rows to
     one shared left edge (Students' Search/Gender/Age/Min age/Max age/
     Sort/Status, Staff's Search/Status/Min £/hr/Max £/hr/Sort, etc.) —
     sized for the longest label across those pages. Invoices' mobile
     filter bar has exactly ONE visible label ("Sort" — Search/Class/
     Status/Payment method/Discount all lost theirs back in the 5 Aug
     2026 pass), so that alignment job does nothing here except pad
     "SORT" out to a box sized for labels like "Min £/hr" that don't
     exist on this page — the dead gap in Rory's mobile screenshot.
     Scoped to #invoice-sort-field (id beats the 3-class rule above
     without !important) so every OTHER .mpa-filter-bar page keeps its
     alignment untouched; the gap also opens slightly (.45rem inherited
     from .mpa-filter-bar .mpa-filter-field is under the 10-16px ask) to
     a deliberate, still-small .75rem (12px) between label and select. */
  #invoice-sort-field.mpa-filter-field {
    gap: .75rem;
  }
  #invoice-sort-field .form-label {
    width: auto;
  }
  /* AI-OPS-1 final acceptance corrective 2 (19 Sep 2026): the three AI
     filter bars (Tenant AI overview / school, Admin Console AI) share
     this component; their longest label ("TIME FRAME") does not fit the
     5.2rem column sized for other pages' labels, so they get a slightly
     wider shared label column -- still one aligned left edge for every
     stacked control, still label LEFT of control. Scoped to
     .ai-filter-bar; no other page changes. */
  .mpa-filter-bar.ai-filter-bar .mpa-filter-field .form-label { width: 6.2rem; }
  .mpa-filter-bar.ai-filter-bar > .btn { width: 100%; justify-content: center; }

  /* ── Page-head action groups opting in (Students/Staff): the surviving
     buttons split the freed row between them. Pairs with the d-none
     d-md-inline-block hiding of Bulk import/export in those templates —
     desktop is untouched. ── */
  .mpa-head-actions { width: 100%; }
  .mpa-head-actions .btn { flex: 1 1 0; }

  /* ── Compaction (item 4, low-stakes): headline figures shrink a step so
     the Invoices summary card fits one phone screen with the filters. ── */
  .mpa-stat { font-size: 1.3rem; }
  .mpa-stat.sm { font-size: var(--mpa-text-lg); }
  .mpa-stat-note { margin-top: .45rem; }
  .mpa-status-list { gap: .9rem 1.2rem; }

  /* ── Invoice detail line items (Rory item 6, 5 Aug 2026 REGRESSION):
     invoices/detail.html's Charge/Qty/Class Total columns are fixed
     percentages of a phone-width card -- narrower than "£84.00" or even
     the word "Charge" itself, so the browser wrapped them mid-figure.
     table-layout:auto lets every column size to its own content instead
     of a forced percentage; nowrap on just the 3 numeric columns (2nd,
     3rd, 4th) then guarantees they can never split, while Description
     (deliberately left OUT of the nowrap rule) is still free to wrap
     across lines as normal -- only the numbers were the reported bug.
     A smaller font on those 3 columns only (12px, still legible) buys
     back enough width that the common case fits without the
     table-responsive wrapper's scrollbar ever needing to engage; that
     wrapper stays as the same safety net every other list table has, for
     the occasional invoice with a wider total than this dev dataset
     tested. Desktop is untouched -- both rules are inside this same
     max-width:767.98px block as everything else above. */
  .mpa-invoice-lines { table-layout: auto !important; }
  .mpa-invoice-lines th:nth-child(2), .mpa-invoice-lines td:nth-child(2),
  .mpa-invoice-lines th:nth-child(3), .mpa-invoice-lines td:nth-child(3),
  .mpa-invoice-lines th:nth-child(4), .mpa-invoice-lines td:nth-child(4) {
    white-space: nowrap;
    font-size: var(--mpa-text-sm);
  }
  /* Mobile numeric-consistency fix (Rory, 7 Aug 2026 — second visual-
     regression report): two numeric surfaces were NOT covered by the
     td:nth-child(2/3/4) rule above, so they stayed at their unrelated
     default sizes instead of matching the .75rem every other Charge/Qty/
     Class Total value uses on a phone width:
       1. The editable Class Total <input> (draft rows only) sets its own
          font-size directly via Bootstrap's .form-control-sm (.875rem) —
          an element's own declared font-size always wins over a parent
          td's, so it never inherited the .75rem above regardless of
          nth-child matching.
       2. The grand Total cell in <tfoot> was never actually reachable by
          td:nth-child(4): that row's first <td> has colspan="3" but is
          still ONE element, so the row only has 3 real <td>s (breakdown,
          Total, blank) — nth-child(4) matches nothing there. Confirmed by
          reading the markup, not assumed. Targeted directly by position
          (tfoot td:nth-child(2), the actual Total cell) instead of
          patching the existing selector list with a wrong index.
     Both now use the SAME literal .75rem already proven not to force a
     wrap at 390px, so "one consistent readable mobile size" is provably
     true (identical value), not just visually similar. */
  .mpa-invoice-lines input[type="number"] { font-size: var(--mpa-text-sm); }
  .mpa-invoice-lines tfoot td:nth-child(2) { font-size: var(--mpa-text-sm); }

  /* Curriculum library staleness form (6 Aug 2026 mobile audit): label +
     80px number input + "months" text + Save button had no wrap, running
     ~32px past the 390px viewport — the page's overflow-x:clip guard then
     silently hid the tail ("months" read as "mon", Save was unreachable).
     Wrapping only below 768px leaves the desktop one-row layout untouched. */
  .mpa-staleness-form { flex-wrap: wrap; }

  /* Tag/info pill badges (Class Plans focus-skill/target-grade, Curriculum
     document tags, Settings grade-usage count — 6 Aug 2026 mobile audit):
     Bootstrap's .badge default white-space:nowrap ran any badge whose own
     text was longer than the phone viewport straight off the edge, clipped
     by the page's overflow-x:clip guard with no visual indication at all
     (no ellipsis, no scroll). These pills already sit in flex-wrap
     containers so wrapping the text inside a pill (rather than truncating
     it) matches the standing wrap-not-truncate rule. Scoped to
     .rounded-pill.text-bg-light so status/count chips elsewhere
     (.mpa-chip, plain .badge.text-bg-warning etc.) are untouched. */
  .badge.rounded-pill.text-bg-light {
    white-space: normal;
    max-width: 100%;
  }
}

/* Invoice-detail line-items typography (Rory, 7 Aug 2026 — visual-
   regression report against an older screenshot): the editable Description
   and Class Total <input>s on a draft invoice carry Bootstrap's
   .form-control-sm (font-size: .875rem), rendering visibly smaller than
   the ~1rem plain text everywhere else in the same row (read-only
   Description, read-only Class Total <span> on approved/sent/paid rows).
   Root-caused against git history before touching anything: this was NOT
   introduced by d7faff0 (that commit's only change to this file/table was
   an unrelated <select> retrofit elsewhere on the page; `git log -S` shows
   .mpa-invoice-lines' only CSS, the mobile block just above, landed whole
   in 11ec6b4 and was never touched again) — .form-control-sm here predates
   all of that. Scoped to .mpa-invoice-lines specifically (NOT a bare
   .form-control-sm override, which is used in forms across the entire
   app) so no other page's inputs are affected, and gated to the exact
   complement of the 767.98px mobile breakpoint above so the anti-wrap
   fix that breakpoint protects (fixed-percentage columns narrower than
   their own content at phone width) is left completely alone. */
@media (min-width: 768px) {
  .mpa-invoice-lines .form-control-sm { font-size: 1rem; }
}

/* Editable Class Total centring (Rory, 7 Aug 2026 — third visual-
   regression report): "319.00" sat visibly left of the Charge/Qty/£9.57/
   Total centre axis despite text-align:center, and despite the input's
   own box being provably centred in its cell (getBoundingClientRect on
   live DOM: input box centre == td centre == the admin-fee <span>'s
   centre, to within 0.01px — checked before writing any CSS). Isolated
   the cause with a mirror element: a plain, hidden <div> built with the
   exact same width/padding/border/font as the live input (but NOT a real
   <input>) predicts "319.00" centring to within 0.005px of that identical
   box's own centre — i.e. the padding/width/font math is already correct,
   so a plain text surface with this exact box would centre perfectly.
   The offset only appears on the real <input type="number">, which
   isolates the cause to Chromium's native step/spin-button control: it
   reserves internal layout space for the up/down arrows that a mirror div
   can't reproduce (it's not part of any inspectable box-model property),
   and setting appearance:none on the INPUT ITSELF (already true here,
   confirmed via getComputedStyle, likely from Bootstrap's own reset)
   does NOT remove it — only targeting the spin-button's own pseudo-
   elements does; a well-documented Chromium quirk, not a guess. Applied
   at ALL widths (not gated to desktop) since centring must hold at every
   width per the required result, incl. the 390px check. -moz-appearance
   covers the equivalent Firefox control. Scoped to .mpa-invoice-lines so
   no other numeric input anywhere else in the app is affected; verified
   via getBoundingClientRect that hiding the spin buttons does not change
   the input's own box geometry (still matches the td/admin-fee centre),
   so this is a pure visual fix with no layout side effect. */
.mpa-invoice-lines input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
  /* width:100% !important (7 Aug 2026): the box this input fills is
     governed by an inline style="width:100px" on the element (a separate,
     older, uncommitted fix — see the template comment at that input) —
     this class-based !important rule beats that plain inline style,
     so centring holds regardless of whether that inline style still says
     100px or has since been swapped for a template-side w-100 class. */
  width: 100% !important;
}
.mpa-invoice-lines input[type="number"]::-webkit-inner-spin-button,
.mpa-invoice-lines input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Invoice Line Edit V1 (15 Sep 2026): the draft Charge / Qty editors keep
   Bootstrap's ordinary bordered .form-control look (so they read as
   editable next to the plain, derived Class Total) but are capped so a
   two-digit quantity doesn't sit in a box the full width of its column;
   margin:auto keeps them on the column's centre axis (the 7 Aug 2026
   alignment ruling). Above the phone breakpoint only — the phone layout
   sizes them inline in its own block. */
@media (min-width: 576px) {
  /* "£ [8.75]" — the pound sign sits beside the Charge input (the same £
     the read-only rendering shows), so the box is unmistakably money;
     the pair is centred on the column axis by the cell's text-align. */
  .mpa-invoice-lines td.mpa-li-rate-edit { white-space: nowrap; }
  .mpa-invoice-lines td:nth-child(2).mpa-li-rate-edit::before { content: "£"; margin-right: .2rem; }
  .mpa-invoice-lines input.mpa-line-unit,
  .mpa-invoice-lines input.mpa-line-qty {
    display: inline-block;
    vertical-align: middle;
    margin: 0;
  }
  .mpa-invoice-lines input.mpa-line-unit { width: 5.5rem !important; }
  .mpa-invoice-lines input.mpa-line-qty { width: 4rem !important; }
}

/* Editable description: an <input> replaced with a <textarea rows="1">
   (V1A final acceptance corrective, Part 7 residual fix, 25 Aug 2026 —
   see the template comment at this field) so it CAN wrap on a phone.
   At this default/desktop size it stays visually identical to the old
   single-line input — nowrap, fixed one-line height, no resize handle,
   no visible scrollbar for the common case (a long value still scrolls
   internally on focus/type, same as the old input always did). Only the
   phone media block below changes any of this. */
.mpa-invoice-desc {
  resize: none;
  overflow: hidden;
  white-space: nowrap;
  height: 1.9rem;
  /* Invoice Line Edit V1 final polish (17 Sep 2026, Rory's alignment
     re-report on the discount/admin-fee rows specifically): a <textarea>
     does NOT vertically centre a single line of its own text via
     line-height the way a normal block/inline element does -- proven
     in-browser (forcing line-height up to 60px on a live element left the
     text pinned to the textarea's own TOP edge; the box grew, the text
     never moved). The OUTER box was already correctly centred within its
     <td> (the 7 Aug / 15 Sep align-middle fixes, which is why plain CLASS
     rows -- an <a>, not a <textarea> -- already looked right); the
     "sits too high" Rory saw on the green/yellow rows was the text
     sitting at the top of ITS OWN taller box, not a <td> problem. Fixed
     the only way that reliably works for a textarea: an explicit
     single-line-height (1.45rem -- matches this context's own natural
     reading line-height, measured live against the class-name links
     above it, so the description reads at the same size/rhythm) plus a
     calculated padding-top that pushes that one line down to the centre
     of the fixed 1.9rem box. padding-bottom stays 0 -- with a FIXED
     height and overflow:hidden, only padding-top affects the text's
     visible position. */
  line-height: 1.45rem;
  padding-top: calc((1.9rem - 1.45rem) / 2) !important;
  padding-bottom: 0 !important;
}

/* ── Invoice line items on true phone widths — the CANONICAL treatment
   (V1A final invoice visual ACCEPTANCE corrective, 25 Aug 2026, round 5).
   The parent portal invoice is the confirmed house reference; this block
   brings the admin table to that same calm standard. Per Rory's explicit
   spec, a normal billable line consumes AT MOST two compact rows:

       Class F                                      <- .9rem semibold, wraps
       £10.00 × 11                    £110.00  [x]  <- one aligned line

   and rate-less rows (discount / admin fee / ad-hoc) are ONE row —
   label left, value right — never a fake "— × —". Every money figure
   right-aligns to ONE shared edge: td5 is a fixed-width delete RAIL
   (--inv-rail) present on every row (empty on rows with no delete), so
   totals line up whether or not a row is deletable, and the mobile
   financial summary below the table (template block .mpa-inv-msummary —
   the tfoot's fused "Subtotal: £X" prose can't be restructured by CSS,
   so phones render a real label/value block instead and the tfoot hides
   here) aligns its values to the same edge by reserving the same rail.
   Same DOM/inputs as desktop — layout mode only. 576–767.98px keeps the
   earlier tuned table; desktop untouched. */
@media (max-width: 575.98px) {
  .mpa-invoice-lines,
  .mpa-invoice-lines tbody {
    display: block;
    width: 100%;
    --inv-rail: 1.7rem;
  }
  .mpa-invoice-lines thead { display: none; }
  /* The structured mobile summary replaces the tfoot entirely here. */
  .mpa-invoice-lines tfoot { display: none; }

  /* One flex-wrap row per line item. Row tint (admin fee amber /
     discount green) moves from Bootstrap's per-cell paint to the row
     itself — --bs-table-bg is scoped on the tr by .table-warning/
     .table-success, so reading it here keeps the exact same colours;
     per-cell paint is cleared so flex gaps don't stripe a tinted row. */
  .mpa-invoice-lines tbody tr {
    display: flex; flex-wrap: wrap; align-items: center;
    column-gap: .35rem; row-gap: .1rem;
    padding: .65rem 1rem;
    border-bottom: 1px solid var(--mpa-hairline);
    background-color: var(--bs-table-bg);
  }
  /* Invoice Line Edit V1 final polish (17 Sep 2026, item 2 -- Rory's
     mobile re-report): on a normal two-line class row (name, then
     Price × Qty / Class Total), the description sat with ~10px above it
     but only ~2px before the Price/Qty line beneath -- measured live,
     Playwright bounding boxes -- reading as "glued to the row below" and
     "floating away from" the separator above it, rather than belonging
     to its own row. Total vertical padding is UNCHANGED (.65rem top +
     .65rem bottom == .45rem + .85rem == 1.3rem), so the row's own height
     and its touch target are byte-identical -- only the SPLIT between top
     and bottom moves, nudging the name up toward the midpoint between the
     separator and the controls beneath it. Scoped to ordinary class rows
     only (:not(:has(td.mpa-li-blank))) -- a rate-less row (discount /
     admin fee / ad-hoc) is a single flex line with its own different
     rhythm, untouched by this. */
  .mpa-invoice-lines tbody tr:not(:has(td.mpa-li-blank)) {
    padding-top: .45rem;
    padding-bottom: .85rem;
  }
  .mpa-invoice-lines tbody td {
    display: block; width: auto;
    padding: 0 !important;
    border: none !important;
    background-color: transparent !important;
    box-shadow: none !important;
    white-space: normal !important;
  }
  /* Empty-state row (single colspan cell) keeps its own centred look. */
  .mpa-invoice-lines tbody tr:has(> td[colspan]) { display: block; border-bottom: none; }
  .mpa-invoice-lines tbody tr:has(> td[colspan]) > td { width: 100%; }

  /* Line 1 — the description: full width, one consistent type size with
     the money line below (only weight/colour differ — Rory: "consistent
     typography between lines"). */
  .mpa-invoice-lines tbody td:first-child {
    flex: 1 1 100%;
    font-weight: 600;
    font-size: var(--mpa-text-base) !important;
    line-height: 1.3;
  }
  /* Rate-less rows (both charge+qty cells carry .mpa-li-blank): ONE
     compact row — label takes the flexible space, value right on the
     same line. flex-BASIS 0 (not auto) is load-bearing: with basis auto
     a long label ("Term discount (Sibling discount)") claims its full
     content width before shrink applies, and flex-wrap bumps the delete
     rail onto its own line; basis 0 lays the label out from nothing and
     grows it into ONLY the space the value+rail leave, wrapping its text
     internally instead (caught visually, round 5). */
  .mpa-invoice-lines tbody tr:has(td.mpa-li-blank) td:first-child {
    flex: 1 1 0;
    min-width: 0;
  }
  /* The editable description <textarea> (see .mpa-invoice-desc above):
     wraps and shows its full value; field-sizing:content (Chromium 123+)
     keeps it content-tight — one line for a short value; the fallback is
     a fixed two-line box, fully readable. */
  .mpa-invoice-lines tbody td:first-child .mpa-invoice-desc {
    width: 100% !important;
    min-width: 0 !important;
    font-size: var(--mpa-text-base) !important;
    font-weight: 600;
    white-space: normal !important;
    overflow: visible !important;
    height: auto !important;
    min-height: 2.4rem;
    line-height: 1.3;
  }
  @supports (field-sizing: content) {
    .mpa-invoice-lines tbody td:first-child .mpa-invoice-desc {
      field-sizing: content;
      min-height: 1.5rem;
    }
  }

  /* Line 2, left — "£10.00 × 11" as one muted unit (the × from qty's
     ::before, nowrap so the unit can never split internally). */
  .mpa-invoice-lines tbody td:nth-child(2),
  .mpa-invoice-lines tbody td:nth-child(3) {
    font-size: var(--mpa-text-sm) !important;
    color: var(--mpa-muted);
    white-space: nowrap !important;
  }
  .mpa-invoice-lines tbody td:nth-child(3)::before { content: "× "; }
  .mpa-invoice-lines tbody td.mpa-li-blank { display: none !important; }
  /* Invoice Line Edit V1 (15 Sep 2026): on a draft the same "£charge × qty"
     phrase is two compact inline inputs — the £ prefix moves to the cell's
     ::before (the input can't carry it), the × stays on the qty cell. Fixed
     small widths keep the phrase + the derived total + the delete rail
     inside a 390px row with no horizontal overflow. */
  .mpa-invoice-lines tbody td:nth-child(2).mpa-li-rate-edit::before { content: "£"; }
  .mpa-invoice-lines tbody td.mpa-li-rate-edit input[type="number"] {
    display: inline-block;
    width: 4.6rem !important;
    min-width: 0;
    padding: .15rem .3rem;
    font-size: var(--mpa-text-md) !important;
    text-align: center;
  }
  .mpa-invoice-lines tbody td:nth-child(3).mpa-li-rate-edit input[type="number"] {
    width: 3.4rem !important;
  }

  /* Line 2, right — the line total, right-aligned to the shared money
     edge (margin-left:auto pushes it against the fixed delete rail). */
  .mpa-invoice-lines tbody td:nth-child(4) {
    margin-left: auto;
    font-weight: 600;
    font-size: var(--mpa-text-base) !important;
    font-variant-numeric: tabular-nums;
    white-space: nowrap !important;
  }
  .mpa-invoice-lines tbody td:nth-child(4) input[type="number"] {
    /* inline-block: Bootstrap's .form-control is display:block, which
       forced a line break between the £ ::before prefix and the number
       (measured live, round 4). */
    display: inline-block;
    width: 5rem !important;
    text-align: right;
    font-size: var(--mpa-text-base) !important;
    font-weight: 600;
  }
  .mpa-invoice-lines tbody td:nth-child(4):has(> input)::before {
    content: "£";
    font-weight: 600;
  }
  @supports (field-sizing: content) {
    .mpa-invoice-lines tbody td:nth-child(4) input[type="number"] {
      field-sizing: content;
      width: auto !important;
      min-width: 2rem;
      max-width: 7rem;
    }
  }

  /* The delete RAIL — fixed width on EVERY row (empty on undeletable
     rows), so all money figures share one right edge and the icons form
     their own tidy column. The <a> gets a real tap target (its own
     padding) while the icon stays visually secondary. */
  .mpa-invoice-lines tbody td:last-child {
    width: var(--inv-rail);
    flex: 0 0 var(--inv-rail);
    text-align: right;
    align-self: center;
  }
  .mpa-invoice-lines tbody td:last-child a {
    display: inline-block;
    padding: .4rem 0 .4rem .4rem;
    line-height: 1;
  }

  /* Structured financial summary (template block, phones only): labels
     left, values right-aligned to the SAME money edge as the line totals
     (matching right rail), Total strongest with a closing rule. */
  .mpa-inv-msummary {
    padding: .7rem 1rem .9rem;
    border-top: 2px solid var(--mpa-line);
  }
  .mpa-inv-msrow {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 1rem;
    padding: .18rem 0;
    font-size: var(--mpa-text-md);
  }
  .mpa-inv-msrow > span:first-child { color: var(--mpa-muted); font-weight: 500; }
  .mpa-inv-msrow > span:last-child {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    padding-right: calc(var(--inv-rail, 1.7rem) + .35rem);
  }
  /* Tinted rows (discount green / admin fee amber) colour BOTH halves —
     the row classes set `color`, spans inherit. */
  .mpa-inv-msrow.mpa-text-ok > span:first-child,
  .mpa-inv-msrow[style] > span:first-child { color: inherit; }
  .mpa-inv-msrow.total {
    margin-top: .35rem;
    padding-top: .5rem;
    border-top: 1px solid var(--mpa-line);
    font-size: 1rem;
  }
  .mpa-inv-msrow.total > span:first-child { color: var(--mpa-ink); font-weight: 700; }
  .mpa-inv-msrow.total > span:last-child { font-weight: 800; }

  /* Note + Save footer: its own breathing room so it reads as the card's
     closing action section, not another welded-on row. */
  .card:has(.mpa-invoice-lines) > .card-footer {
    padding: .95rem 1rem 1.1rem;
  }
}
/* The mobile summary block must never leak into sm+ (it's d-sm-none) and
   the tfoot never leaks below sm — the pairing lives in the media block
   above; nothing to declare at desktop. */

/* ── Admin invoice LIST on true phone widths (final visual acceptance
   corrective Part D, 25 Aug 2026). The generic phone table treatment
   (40rem floor + frozen name column) technically survived but the frozen
   cell's own chips ("1/2", "Accounting") collided with the sliding
   columns — horizontal-table survival is not responsive design for an
   operational screen. Each row becomes one deliberate composition:

       [x] Eva Ashworth                     £794.00
           Invoice 1 of 2
       [Sent] [Stale]                  [Accounting]

   Same DOM (the row checkbox is the single real input — nothing
   duplicated), whole-row click-through (mpaRowNav) intact, the name
   still the primary link. Subtotal/Discount/Payment-method/actions are
   phone-hidden — the detail page owns that depth, and the row itself IS
   the link to it. Desktop table untouched. */
@media (max-width: 575.98px) {
  /* Mode switch: this table opts OUT of the generic scroll+freeze phone
     treatment entirely (static !important beats the sticky rules; the
     40rem floor is lifted by the more specific :has selector). */
  .table-responsive:has(.mpa-table.mpa-inv-list) > .mpa-table.table {
    min-width: 0;
  }
  .mpa-table.table.mpa-inv-list thead th,
  .mpa-table.table.mpa-inv-list tbody td {
    position: static !important;
    background: transparent !important;
  }
  .mpa-inv-list thead { display: none; }

  .mpa-inv-list, .mpa-inv-list tbody, .mpa-inv-list tfoot {
    display: block; width: 100%;
  }
  /* Invoice + Reports Interaction Polish V2 amendment (5 Sep 2026) --
     ROOT CAUSE of the malformed iPhone list: the previous phone
     composition addressed cells by nth-child POSITION (1..8) and hid
     4/5/7/8; V2's All-terms Term column shifted those positions, and
     worse, V2's own comment above that hide rule was closed with `#}`
     (Jinja syntax) instead of a CSS star-slash, so the parser swallowed the
     entire hide rule into an unterminated comment -- subtotal, discount,
     payment method, Term AND the View button all fell back to plain
     flex items with order:0 and rendered as an uncontrolled horizontal
     run beside the checkbox, overlapping the name/total (measured:
     every one of those cells computed display:block at 390px).

     Structural fix: each cell carries a semantic class (invoices/
     _results.html: .inv-c-name/.inv-c-total/...) and the row is a CSS
     GRID keyed on those classes, never on column position -- inserting
     or removing a column (Term) cannot scramble it. One repeatable card:

         [x] Student name                          £1,174.20
             Invoice 1 of 2
         Summer Term (seed)                  (All terms only)
         [Paid] [Accounting] [Stale] [Review required]
         Subtotal ......................... £1,236.00
         Discount ........................... −£61.80   (only if any)
         Payment method ............... Card (Stripe)
         [ View ]  [ Void… ]

     Same DOM, same links, same checkbox, same whole-row mpaRowNav. */
  /* Final amendment §G (5 Sep 2026): ONE left alignment line. The grid's
     column-gap used to indent the name past the (possibly EMPTY) checkbox
     column -- measured 72px vs 49px on the selected-term view, 59px vs
     49px on All terms where the cell holds no checkbox at all. Now the
     gap lives on the checkbox INPUT itself (so an empty cell costs 0px),
     and every secondary line (term, chips, detail rows) starts in the
     name's own column, so name / "Invoice N of M" / term / chips /
     details share one left edge. Actions stay full-width; total stays
     right-aligned. */
  .mpa-inv-list tbody tr {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    column-gap: 0; row-gap: 0;
    align-items: start;
    padding: .7rem 1rem;
    border-bottom: 1px solid var(--mpa-hairline);
  }
  .mpa-inv-list tbody td {
    display: block; width: auto; min-width: 0;
    padding: 0 !important; border: none !important;
    box-shadow: none !important;
    text-align: left !important;
  }
  /* Row 1: checkbox · name block · total due. */
  .mpa-inv-list tbody td.inv-c-check { grid-row: 1; grid-column: 1; padding-top: .15rem !important; }
  .mpa-inv-list tbody td.inv-c-check .form-check-input { margin-right: .6rem; }
  .mpa-inv-list tbody td.inv-c-name  { grid-row: 1; grid-column: 2; overflow-wrap: anywhere; }
  .mpa-inv-list tbody td.inv-c-name a { display: inline; font-size: var(--mpa-text-base); }
  .mpa-inv-list tbody td.inv-c-total {
    grid-row: 1; grid-column: 3; margin-left: .6rem;
    font-weight: 700; font-size: var(--mpa-text-base);
    font-variant-numeric: tabular-nums; white-space: nowrap;
    text-align: right !important;
  }
  /* "Invoice N of M" sits directly under the name, always the same place
     (the brief's "aligned consistently"), never on the chip line. */
  .mpa-inv-list .mpa-inv-seqline {
    display: block; margin-top: .1rem;
    font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-weight: 500;
  }
  /* Row 2 (All terms only): the term, one muted line. */
  .mpa-inv-list tbody td.inv-c-term {
    grid-row: 2; grid-column: 2 / -1;
    margin-top: .3rem; font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-weight: 500;
  }
  /* Row 3: lifecycle + secondary chips, grouped left, wrapping. */
  .mpa-inv-list tbody td.inv-c-status {
    grid-row: 3; grid-column: 2 / -1;
    display: flex; flex-wrap: wrap; align-items: center; gap: .3rem;
    margin-top: .45rem;
  }
  /* Lifecycle chip first, Accounting/Stale/Review after (flex order,
     DOM order unchanged). */
  .mpa-inv-list .mpa-inv-acct-m { margin-left: 0 !important; order: 10; }
  /* Rows 4-6: label : value detail lines (label from data-label, same
     mechanism as .mpa-table-stack elsewhere on Reports). */
  .mpa-inv-list tbody td.inv-c-subtotal,
  .mpa-inv-list tbody td.inv-c-discount,
  .mpa-inv-list tbody td.inv-c-fee,
  .mpa-inv-list tbody td.inv-c-payment {
    grid-column: 2 / -1;
    display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
    font-size: var(--mpa-text-sm); font-variant-numeric: tabular-nums;
    padding: .18rem 0 !important;
  }
  /* Invoice Line Edit V1 true-final corrective (17 Sep 2026): rows 4-7 are
     Subtotal / named discounts / Admin fee / Payment method; a row with
     nothing to say (is-empty) is dropped, so the card stays compact. */
  .mpa-inv-list tbody td.inv-c-subtotal { grid-row: 4; margin-top: .5rem; border-top: 1px solid var(--mpa-hairline) !important; padding-top: .45rem !important; }
  .mpa-inv-list tbody td.inv-c-discount { grid-row: 5; }
  .mpa-inv-list tbody td.inv-c-fee      { grid-row: 6; }
  .mpa-inv-list tbody td.inv-c-payment  { grid-row: 7; }
  .mpa-inv-list tbody td.inv-c-subtotal::before,
  .mpa-inv-list tbody td.inv-c-discount::before,
  .mpa-inv-list tbody td.inv-c-fee::before,
  .mpa-inv-list tbody td.inv-c-payment::before {
    content: attr(data-label); color: var(--mpa-muted); font-weight: 500;
    font-variant-numeric: normal; flex: 0 0 auto;
  }
  /* Invoice Line Edit V1 final polish (17 Sep 2026, item 5): the "Admin
     fee" LABEL itself reads amber too, not just its amount -- matching
     the invoice detail page's own mobile summary row, which colours
     label+value together for this exact concept (both spans inherit one
     parent colour there). Scoped to .inv-c-fee only -- Subtotal/Discount/
     Payment-method labels stay the neutral muted tone above. */
  .mpa-inv-list tbody td.inv-c-fee::before {
    color: var(--mpa-warn);
  }
  .mpa-inv-list tbody td.inv-c-discount.is-empty,
  .mpa-inv-list tbody td.inv-c-fee.is-empty,
  .mpa-inv-list tbody td.inv-c-payment.is-empty { display: none; }
  /* Named adjustments replace the generic "Discount −£X" row when the
     breakdown is known: one label/value line per discount (stacked
     discounts = separate rows), values on the same right edge as
     Subtotal and Admin fee. */
  .mpa-inv-list tbody td.inv-c-discount.has-named { display: block; }
  .mpa-inv-list tbody td.inv-c-discount.has-named::before,
  .mpa-inv-list tbody td.inv-c-discount.has-named > .mpa-inv-discount-why { display: none; }
  .mpa-inv-list tbody td.inv-c-discount.has-named .mpa-inv-adj-rows { display: block; }
  .mpa-inv-list .mpa-inv-adj-row {
    display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
    font-size: var(--mpa-text-sm); font-variant-numeric: tabular-nums;
  }
  .mpa-inv-list .mpa-inv-adj-row > span:first-child { font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
  .mpa-inv-list .mpa-inv-adj-row > span:last-child { white-space: nowrap; }
  /* Final surgical corrective (§3, banking pass): Discount alone gets
     flex-wrap so a full-width reasons line can drop below label+amount
     without disturbing Subtotal/Payment (single short words, never wrap).
     The amount stays a fixed, never-shrinking, never-wrapping flex item;
     the reasons line is forced onto its OWN row via flex-basis:100% (the
     standard "always starts a new flex line" technique) and is itself
     allowed to shrink/wrap (min-width:0 -- flex items default to
     min-width:auto, i.e. NEVER shrink below their content's natural
     width, which was the actual root cause: a joined "reason1, reason2"
     string at flex:0 0 auto could only ever grow the row, never wrap). */
  .mpa-inv-list tbody td.inv-c-discount { flex-wrap: wrap; }
  .mpa-inv-list tbody td.inv-c-discount .mpa-inv-discount-why {
    flex: 0 0 auto; white-space: nowrap;
  }
  .mpa-inv-list tbody td.inv-c-discount .mpa-inv-discount-reasons {
    flex: 1 1 100%; min-width: 0; margin-top: .1rem;
    overflow-wrap: anywhere; word-break: break-word;
    font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-weight: 500;
    text-align: left;
  }
  /* Row 8: actions -- full-width, thumb-sized targets, never competing
     with the name/total line above.

     INVOICE-MOBILE-POLISH-1 FINAL MICRO-POLISH (19 Sep 2026, §3): this
     was `grid-row: 7` -- the SAME row number as .inv-c-payment two rules
     up. Harmless for the common case, because .inv-c-payment.is-empty is
     display:none (draft/approved/most invoices never show a payment
     method row at all, so row 7 has only ever had one real occupant in
     testing) -- but a genuinely PAID invoice's real, non-empty Payment
     method row and this actions row were landing in the identical grid
     cell, rendering on top of each other (Rory's real repro: Chloe
     Ashworth's paid card, "Payment method / Cash" sitting over the View
     button). Root cause was this literal duplicate row index, not a
     missing height/spacing rule -- fixed by giving actions its own row,
     one past the last real detail row (subtotal=4/discount=5/fee=6/
     payment=7), so CSS Grid can never stack the two on top of each
     other again regardless of which detail rows are present that card. */
  .mpa-inv-list tbody td.inv-c-actions {
    grid-row: 8; grid-column: 1 / -1;
    display: flex; gap: .5rem; margin-top: .55rem;
  }
  .mpa-inv-list tbody td.inv-c-actions .btn {
    flex: 1 1 0; min-height: 38px; margin: 0 !important;
    display: inline-flex; align-items: center; justify-content: center;
  }

  /* Footer: count left, filtered total right — one calm closing line. */
  .mpa-inv-list tfoot tr {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 1rem; padding: .6rem 1rem;
    border-top: 2px solid var(--mpa-line);
  }
  .mpa-inv-list tfoot td {
    display: block; width: auto;
    padding: 0 !important; border: none !important;
  }
  /* Final amendment §F: class-keyed like the body cells. Phones keep the
     calm two-item footer (count · Total due); the Subtotal/Discount
     totals are desktop-column totals and stay desktop-only here. */
  .mpa-inv-list tfoot td.inv-f-total {
    font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  .mpa-inv-list tfoot td:not(.inv-f-count):not(.inv-f-total) { display: none; }
}

/* §2/§3 (Invoice + Reports Interaction Polish V2): a small breathing gap
   above the anchor target so the native #invoice-results scroll (from a
   Reports/Dashboard status-tile link, or the in-page JS scroll below)
   never lands the filtered rows flush against the viewport's top edge. */
#invoice-results { scroll-margin-top: 12px; }
/* Amendment §B (5 Sep 2026): the Class combo's INPUT no longer needs to
   be as wide as its longest class name -- its dropdown LIST sizes to its
   own content (an absolutely-positioned box may exceed its containing
   block when given an explicit max-content width) so long names still
   read on one line while the bar fits one desktop row. Scoped to the
   Invoices bar; every other combo keeps its existing behaviour. */
#invoice-filters .mpa-combo-list { width: max-content; min-width: 100%; max-width: min(360px, calc(100vw - 2rem)); }

/* Final amendment §A (5 Sep 2026): when the SELECTED term is the current
   term, the whole <select> control reads semibold -- the one treatment a
   native select honours everywhere (desktop + iOS), unlike per-<option>
   styling. partials/_term_select.html adds the class; historical /
   "All terms" selections keep the normal weight. The option rule below is
   a harmless bonus where the open list honours it (Chromium/Firefox). */
select.is-current-term,
.mpa-filter-bar select.is-current-term { font-weight: 700; }  /* (0,2,1) beats the bar's own weight:500 rule */
select option.is-current-term { font-weight: 700; }

/* Final amendment §H (5 Sep 2026), corrected in the final surgical pass
   (§4, banking): the original fix wrapped the CTA onto its own line
   because "Follow up with trialists" was too long a phrase for a
   one-row head -- the banking pass shortened the CTA to "Follow up →"
   instead (same length class as "Attendance not submitted"'s own
   "View all →"), so the head no longer needs a stacking modifier and
   dashboard.html now uses the plain shared .mpa-card-head. The row-note
   stacking below is a SEPARATE, still-needed fix (long "trial ended…"/
   "trial — length not recorded" side notes squeezing "student · class"
   into a 3-line column) and stays. */
@media (max-width: 575.98px) {
  .mpa-rows.mpa-rows-stack-side .mpa-row { flex-wrap: wrap; row-gap: .1rem; }
  .mpa-rows.mpa-rows-stack-side .mpa-row-main { flex: 1 1 100%; }
  .mpa-rows.mpa-rows-stack-side .mpa-row-side { flex: 0 0 100%; text-align: left; margin-left: 0; }
}

/* ════════════════════════════════════════════════════════════════
   DESKTOP EXPLANATIONS: NO INFO ICONS (design system, 21 Sep 2026)
   ════════════════════════════════════════════════════════════════
   Rory's standing rule: on a device that can hover, a KPI label, metric
   label or table heading carries NO visible ⓘ / i / ? / eye glyph. The
   LABEL ITSELF is the hover and keyboard-focus target, and the
   explanation arrives as the normal tooltip. On touch, where there is
   nothing to hover, the glyph becomes a real tappable button.

   ⚠ THIS RULE LIVES HERE, IN THE HEAD STYLESHEET, ON PURPOSE.
   It used to live in partials/_aiu_tips.html, which every page includes
   at the BOTTOM -- so the glyphs painted on first render and vanished
   only once that late <style> and its script had been parsed. A flash of
   exactly the icons Rory asked to be rid of, on every cold load, and
   nothing at all on a page that forgot the partial. In <head> they never
   paint. partials/_aiu_tips.html still REMOVES them from the DOM in JS
   (belt and braces, and it is what makes the label the real target);
   this makes sure they are never seen even for a frame.

   The affordance that replaces the icon is on the words: `cursor: help`
   plus a faint dotted underline, applied by _aiu_tips.html when it wires
   the label. Keyboard focus shows the same tooltip. */
/* ⚠ WIDTH *OR* HOVER (final visual corrective, 21 Sep 2026). This used to
   be `(hover: hover)` alone, and Rory's own Chrome reports (hover: none)
   at 1440px -- touchscreen laptops, 2-in-1s and DevTools emulation all do
   -- so his desktop took the TOUCH branch and grew a tappable (i) beside
   every label while headless Chromium (hover: hover) showed none. A
   desktop is a desktop by its width; the pointer's self-report is only a
   second reason to hide. Both the decorative glyph AND the promoted tap
   button are covered, so no runtime branch can bring one back. */
@media (hover: hover), (min-width: 992px) {
  .aiu-kpi-hint, .aiu-kpi-hint-tap { display: none !important; }
}

/* ════════════════════════════════════════════════════════════════
   THE WHITE DATA-SURFACE RULE (design system, 21 Sep 2026)
   ════════════════════════════════════════════════════════════════
   Rory's standing rule, after three separate pages drifted into grey
   "Excel-like" rows while Students/Classes/Invoices stayed clean:

     * STANDARD DATA ROWS ARE WHITE. The dataset is the content; it does
       not get a tinted background to mark it as a dataset.
     * The PAGE background stays the existing light neutral
       (--mpa-bg) -- the white card is what separates data from page.
     * HOVER is a subtle tint (--mpa-row-hover), never a block of grey.
     * SELECTED/ACTIVE may use an intentional accent tint.
     * DISABLED/SECONDARY may be muted or grey -- grey means "not your
       data", it is not the default for data.
     * HEADERS and FILTER CONTROLS keep the normal control neutral.

   `.mpa-table` is THE component that implements this. A list that wants
   the house look adds the class; it does not restyle rows locally. The
   AI/Support surfaces' grid-based lists (.aiu-req-row, partials/
   _ai_console.html) deliberately reuse the same tokens below so a
   <table> list and a CSS-grid list are indistinguishable to the eye.

   NOT applied globally to forms/controls: this is a rule about DATA
   SURFACES, and recolouring inputs would be a different (and unasked
   for) change. */
:root {
  /* The one hover tint for any data row, table or grid. Was hard-coded
     as #faf9fd in four places (data rows, .mpa-row, the mobile list's
     open item and the ranked-row list); named here so the AI lists and
     the Support queue cannot drift from Students, and so a future change
     of mind is one edit. */
  --mpa-row-hover: #faf9fd;
}
.mpa-table.table { font-size: var(--mpa-text-md); color: var(--mpa-ink); margin-bottom: 0; }
/* White, explicitly. Bootstrap's own --bs-table-bg has changed meaning
   between minor versions; saying it here means a theme upgrade cannot
   quietly grey the dataset again. */
.mpa-table.table > :not(caption) > * > * { background-color: transparent; }
.mpa-table.table tbody tr { background: var(--mpa-surface); }
.mpa-table.table thead th {
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
  color: var(--mpa-muted); background: var(--mpa-surface);
  border-bottom: 1px solid var(--mpa-line);
  padding: .65rem .75rem; white-space: nowrap;
  position: sticky; top: 0; z-index: 2;
}
/* UI-CONSISTENCY-1 (21 Sep 2026): the audit measured two .mpa-tables
   (Class Interest +103px, Attendance +5px) pushing the whole DOCUMENT
   sideways at 1024px -- the :has(.mpa-table) overflow:visible rule that
   keeps headers sticky assumes every table fits above 768px, and the
   nowrap headings were the one thing stopping those two from fitting.
   Letting a heading WRAP below 1200px keeps the sticky header and the
   fit; desktop (>=1200px) headings stay on one line as before. */
@media (max-width: 1199.98px) {
  .mpa-table.table thead th { white-space: normal; }
}
.mpa-table.table thead a { color: inherit !important; text-decoration: none; }
.mpa-table.table thead a:hover { color: var(--mpa-ink-brand) !important; }
.mpa-table.table tbody td { padding: .6rem .75rem; border-color: var(--mpa-hairline); vertical-align: middle; }
.mpa-table.table tbody td.text-end { font-variant-numeric: tabular-nums; }
.mpa-table.table-hover tbody tr:hover td { background: var(--mpa-row-hover); box-shadow: none; }
.mpa-table.table tfoot td {
  border-top: 1px solid var(--mpa-line); background: var(--mpa-row-hover);
  padding: .65rem .75rem; font-variant-numeric: tabular-nums;
}
.mpa-table.table thead.table-light th { background: transparent; }
.mpa-table.table tbody tr.table-warning td { background: #fdf8ee; }

/* ============================================================
   TIMETABLE GRID (§18 Session 2) — dedicated .mpa-grid component.
   DELIBERATELY NOT .mpa-table: the :has(.mpa-table) overflow:visible
   rule above (Students sticky-header fix) would silently kill this
   grid's horizontal scroll, which the timetable REQUIRES. Here the
   grid is a bounded scroll panel instead, so both sticky axes work:
   day-header row sticks to the top, time column sticks to the left —
   that's what makes horizontal scroll feel deliberate, not clunky.
   border-collapse:separate is load-bearing — collapsed borders detach
   from sticky cells and shear while scrolling.
   ============================================================ */
.mpa-grid { overflow: auto; max-height: 78vh; }
.mpa-grid table { border-collapse: separate; border-spacing: 0; margin: 0; width: 100%; }
.mpa-grid thead th {
  position: sticky; top: 0; z-index: 3;
  background: var(--mpa-surface);
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
  color: var(--mpa-muted); text-align: center;
  padding: .6rem .75rem;
  border-bottom: 1px solid var(--mpa-line);
  border-right: 1px solid var(--mpa-line);
}
.mpa-grid thead th:first-child { left: 0; z-index: 4; }
.mpa-grid tbody th {
  position: sticky; left: 0; z-index: 2;
  background: var(--mpa-surface);
  font-size: var(--mpa-text-xs); font-weight: 600; color: var(--mpa-muted);
  font-variant-numeric: tabular-nums; text-align: center; vertical-align: top;
  padding: .2rem .4rem 0;
  border-right: 1px solid var(--mpa-line);
}
/* Empty cells canvas-tinted; cells that hold slot cards stay surface-white
   so the venue-coloured cards sit on a quiet ground. Column separators
   wear the stronger --mpa-line (which day does this class belong to?);
   row separators stay recessive hairlines. */
.mpa-grid tbody td {
  background: var(--mpa-bg);
  border-right: 1px solid var(--mpa-line);
  border-bottom: 1px solid var(--mpa-hairline);
}
.mpa-grid tbody td.slot { background: var(--mpa-surface); }

/* GEOMETRY RECOVERY (19 Aug 2026) — admin band grid (.tt-bandgrid) only;
   the parent portal's own per-week rowspan table deliberately keeps the
   generic .mpa-grid rules above unchanged.
   table-layout:fixed = EQUAL day columns: the Time column takes its
   declared 60px and every visible day shares the remaining width equally
   — card content/lane count can never widen one day past another.
   The one body row's height is the SELECTED WEEK's own time band (set by
   applyWeek(), index.html), so the scroll container's content IS the
   active band and native scrolling needs no clamps. */
.mpa-grid table.tt-bandgrid { table-layout: fixed; }
/* ACCEPTANCE CORRECTIVE #3 (26 Aug 2026) -- this was `position: relative`,
   given ONLY to make .tt-times a positioning context for its absolutely-
   positioned .tt-timelabel children (their `top` offsets need SOME
   ancestor with position != static). That silently overrode the sticky-
   left .tt-times inherits from the generic `.mpa-grid tbody th` rule
   above (a `.tt-bandgrid .tt-times` selector beats a bare `tbody th` one)
   -- so the "TIME" HEADER stayed frozen on horizontal scroll (thead th
   is untouched by this rule) while every actual hour label underneath it
   scrolled away with the day columns, exactly Rory's manual-test finding.
   `position: sticky` establishes the SAME positioning context `relative`
   did (CSS containing-block rules treat sticky identically to relative
   for this purpose) while ALSO keeping the cell pinned during horizontal
   scroll -- one declaration fixes both jobs, no second rule needed. */
.mpa-grid table.tt-bandgrid .tt-times { padding: 0; position: sticky; left: 0; }
.mpa-grid table.tt-bandgrid .tt-timelabel {
  position: absolute; left: 0; right: 0;
  text-align: center; line-height: 1; padding-top: 2px;
}

/* Timetable slot card — background stays the VENUE COLOUR (inline style;
   the "card colour = venue" scanability rule + mpaApplyVenueColour()
   repaints via style.background), so this class carries shape only.
   overflow:hidden (12 Aug 2026, admin desktop-grid overlap fix): a flex
   item's automatic minimum size defaults to its CONTENT's min-content
   height, not its flex-basis — so on a short (e.g. 45-min) card, the
   card's own text content forced it taller than the height its
   absolutely-positioned wrapper had computed from the real duration,
   overflowing into whatever sits immediately below (found live: a
   45-min card measured 20px taller than its wrapper, spilling into the
   next card's box and reading as a false visual overlap even though the
   underlying top/height geometry was already exactly correct).
   overflow:hidden removes that automatic-minimum-size floor, so the
   card can never render taller than the true duration it represents —
   same fix already applied inline on the parent portal's equivalent
   card (portal/timetable.html), now made the ONE shared rule so it
   can't drift between the two surfaces again. Harmless everywhere else
   this class is used (mobile day list / one-off agenda cards have no
   fixed height to overflow in the first place). */
.mpa-slot-card {
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  box-shadow: var(--mpa-shadow);
  overflow: hidden;
  transition: transform .16s ease, box-shadow .16s ease;
}
.mpa-slot-card:hover { transform: translateY(-1px); box-shadow: var(--mpa-shadow-lift); }

/* Venue palette swatch — selected ring (Alpine toggles via :class, never
   :style — a string :style binding rewrites the static style attribute).
   box-shadow, NOT outline (26 July root-cause, browser-verified): an
   outline+offset draws entirely OUTSIDE the border box, and inside the
   colour modal's rounded-clip/scroll container (.modal-content
   overflow:hidden + .modal-body overflow:auto) Chromium fails to track
   that damage — the class toggle painted NOTHING until an unrelated
   click damaged the area, which read as "ring lags until the next
   interaction" and survived two JS rewrites because it was never JS.
   box-shadow damage rides the element's own visual overflow, so it
   repaints on the toggle itself. White inner layer = the old 2px
   outline-offset gap. */
.mpa-swatch-on { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--mpa-action); }

/* Cross-family reserved swatch (final polish pass, 2 Sep 2026): the LIVE
   reactive twin of the server-rendered opacity/cursor inline style, driven
   by Alpine's :class binding (never :style, which would clobber the
   button's own static per-swatch background colour — see the venue swatch
   loop's own comment). Same visual as the pre-existing inline greying. */
.mpa-swatch-reserved { opacity: .25; cursor: not-allowed; }

/* Timetable card fit pass (Timetable UX Overnight Fix V1, 2 Sep 2026): an
   optional card row/chip that the duration-proportional card genuinely
   cannot fit is removed WHOLE -- never half-clipped below the card's edge.
   Toggled by mpaFitCards() (timetable/index.html) from the data-tt-fit
   priorities stamped in timetable/_macros.html. Display-only: colours,
   chip variants and every other rule are untouched. */
.tt-cardw .tt-fit-hidden { display: none !important; }

/* Venue pills as filters (Timetable UX Overnight Fix V1 iteration 2): the
   coloured legend chips above the grid are now GET links that set the
   existing filter_venue param. Same chip, no restyle -- only a pointer and
   the selected-state ring (the swatch ring's own recipe, so "selected"
   reads the same way everywhere on this page). */
.mpa-venue-pill { text-decoration: none; cursor: pointer; }
.mpa-venue-pill:hover { filter: brightness(.96); text-decoration: none; }
.mpa-venue-pill-on { box-shadow: 0 0 0 2px #fff, 0 0 0 3px var(--mpa-action); }

/* Customise colours modal -- room rows sit visually under their venue. */
#colourModal .mpa-room-row td { background: var(--mpa-surface-2, #f8f9fa); border-top: 0; padding-top: .25rem; padding-bottom: .25rem; }
#colourModal .mpa-room-expander { color: var(--mpa-muted); line-height: 1; vertical-align: middle; }

/* ============================================================
   TIMETABLE MOBILE (§18 Session 5) — day strip + week pager,
   filter bottom sheet, full-screen slot drawer below lg.
   Desktop keeps the 460px side drawer; its width lives HERE
   (not inline on the element) so the media query can win.
   ============================================================ */
#slot-drawer { width: 460px; max-width: 92vw; }
/* The drawer body is a VERTICAL scroll surface only. Bootstrap .row
   gutters (the teacher/venue form in panel.html) use negative side
   margins, which otherwise give the body a few px of horizontal
   wiggle — on mobile that reads as the whole page scrolling sideways
   (round-3 fix 1). */
#slot-drawer .offcanvas-body { overflow-x: hidden; }
/* Scroll-jump corrective (27 Aug 2026, proven with a live scrollTop
   trap): htmx's innerHTML swap of the drawer body briefly holds old +
   new content together, and Chrome's native scroll ANCHORING re-anchors
   across the removal — landing scrollTop at the container maximum, so
   every panel swap without an explicit scroll correction "jumped to the
   bottom". overflow-anchor:none is the platform's own opt-out: the
   drawer keeps the admin's numeric scroll position across swaps, and
   the panel's alert/pending scripts remain the only deliberate movers. */
#slot-drawer .offcanvas-body { overflow-anchor: none; }

/* Horizontally scrollable day strip — pill per day, no visible scrollbar */
.mpa-daystrip {
  display: flex; gap: .3rem; overflow-x: auto;
  -webkit-overflow-scrolling: touch; scrollbar-width: none; padding: 2px 0;
}
.mpa-daystrip::-webkit-scrollbar { display: none; }
.mpa-daypill {
  flex: 0 0 auto; border: 1px solid var(--mpa-line); background: var(--mpa-surface);
  border-radius: 999px; padding: .28rem .65rem;
  font-size: var(--mpa-text-sm); font-weight: 600; line-height: 1.2; white-space: nowrap;
  color: var(--mpa-muted);
}
.mpa-daypill.is-today:not(.active) { border-color: var(--mpa-action); color: var(--mpa-action); }
.mpa-daypill.active { background: var(--mpa-action); border-color: var(--mpa-action); color: #fff; }
.mpa-daypill:disabled { opacity: .35; }

/* §18 S6 — week-aware desktop grid. A cell whose every card is hidden
   for the selected week drops back to the empty-cell canvas tint
   (applyWeek() toggles the class); the shared week/day strip becomes a
   compact control block on desktop — full-bleed chevron rows read wrong
   at content width. */
.mpa-grid td.slot.tt-week-empty { background: var(--mpa-bg); }
@media (min-width: 992px) { .mpa-weeknav { max-width: 430px; } }

/* ============================================================
   Shared week navigation (11 Aug 2026 acceptance round 3, items 1+2).
   ONE implementation for Admin/Staff (timetable/index.html) and Parent
   (portal/timetable.html) rather than parallel per-page CSS — same
   colour rule (current week = accent + "· This week"), same stable
   layout (prev arrow | label | next arrow never resize the row), same
   behaviour on mobile (all three stay on ONE row).
   14 Sep 2026 corrective (Rory's acceptance finding 2): the Go-to-this-
   week control that used to live in this strip (.mpa-weeknav-goto/
   -goto-text-full/-short) has moved to each surface's own controls row,
   grouped with Print and the class-type filters — see index.html's and
   portal/timetable.html's own "Go to this week" comments. Removed here
   rather than left dead: nothing renders these classes any more.
   ============================================================ */
.mpa-weeknav-controls { display: flex; align-items: center; gap: .4rem; flex-wrap: nowrap; }
/* Fixed WIDTH (not min-width) — "10 – 16 Aug · This week" and "17 – 23
   Aug" must occupy the identical box so the arrow either side of it
   never shifts; ellipsis is just a safety net for the rare compound
   case (month AND year AND "This week" together). Admin and parent now
   share one date-range formatter (portal.py's _week_range_label mirrors
   index.html's weekLabelFor exactly) specifically so one fixed width
   works for both without either page's text needing to truncate. */
.mpa-weeknav-label {
  width: 190px; flex-shrink: 0;
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: var(--mpa-text-sm); color: var(--mpa-muted);
}
.mpa-weeknav-label.is-current { color: var(--mpa-ink-brand); }
@media (max-width: 420px) {
  .mpa-weeknav-controls { gap: .3rem; }
  .mpa-weeknav-label { width: 108px; font-size: var(--mpa-text-xs); }
}

/* Filter bottom sheet — auto height, rounded top, safe-area padding.
   ⚠ .offcanvas.mpa-sheet, NOT bare .mpa-sheet: Bootstrap's own
   .offcanvas.offcanvas-bottom (0,2,0) sets height:30vh, which BEATS a
   single class (0,1,0) — that's why the sheet first rendered two filters
   tall (S5 fix 4). Matching specificity + later source order wins.
   Labels echo the filter bar's micro-label look (those rules are scoped
   to .mpa-filter-bar, so the sheet needs its own copy). */
.offcanvas.mpa-sheet { height: auto; max-height: 80vh; border-top-left-radius: 14px; border-top-right-radius: 14px; }
.mpa-sheet .offcanvas-body { padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); }
.mpa-sheet select.form-select, .mpa-sheet input.form-control { width: 100%; }
.mpa-sheet .form-label {
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--mpa-muted); margin-bottom: .15rem;
}

/* Student filter combo (§18 S5 fix 1) — full list on focus, narrows as
   you type. Desktop: floating dropdown. In the bottom sheet it goes
   in-flow instead (absolutely-positioned children don't grow a scroll
   container, so a floating list would be clipped by the sheet body). */
.mpa-combo { position: relative; min-width: 170px; }
.mpa-combo-list {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 1080;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: 10px; box-shadow: var(--mpa-shadow-lift);
  max-height: 260px; overflow-y: auto; padding: 4px;
}
.mpa-combo-item {
  display: block; width: 100%; text-align: left; border: 0; background: transparent;
  padding: .35rem .55rem; border-radius: 7px; font-size: var(--mpa-text-sm); color: var(--mpa-ink);
  /* Manual-acceptance fix (17 Aug 2026): a long underscore-joined class
     name (e.g. QA_S1R2_GROUP_316a5a) has no space/hyphen to wrap at, so
     it overflowed this 100%-width button horizontally — and because
     .mpa-combo-list already sets overflow-y:auto, the spec forces the
     OTHER axis to compute as auto too, which is exactly where the
     "ugly horizontal scrollbar" came from. overflow-wrap:anywhere lets
     an unbroken token break as a last resort, same as every other
     wrapping label in this app; normal short labels are unaffected. */
  white-space: normal;
  overflow-wrap: anywhere;
}
.mpa-combo-item:hover { background: var(--mpa-hairline); }
.mpa-combo-item.on { background: var(--mpa-track); font-weight: 600; }
.mpa-combo-empty { padding: .35rem .55rem; font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
.mpa-sheet .mpa-combo-list { position: static; margin-top: 4px; max-height: 40vh; }

/* Session-note editor (§15 MVP 3, 29 July 2026 mobile fix): at lg+ the
   panel has NO positioning — it stays an inline expanding row in the
   roll-call table. Below lg it becomes a fixed bottom sheet (same DOM,
   fixed children leave the table flow) with a tap-to-close backdrop.
   z-index sits above the sticky header (1020) and sidebar, below toasts. */
@media (max-width: 991.98px) {
  .mpa-note-backdrop { position: fixed; inset: 0; z-index: 1050; background: rgba(0, 0, 0, .4); }
  .mpa-note-panel {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 1055;
    background: var(--mpa-surface, var(--bs-body-bg));
    border-top-left-radius: 12px; border-top-right-radius: 12px;
    box-shadow: 0 -0.5rem 1rem rgba(0, 0, 0, .25);
    padding: .75rem 1rem 1rem;
    max-height: 70vh; overflow-y: auto;
  }
  /* Comms/Prospect polish round 2 (20 Oct 2026) — Rory's iPhone: tapping INTO the note made the sheet run
     off the right edge. Root cause: the textarea is .form-control-sm (14px), and the app-wide iOS guard
     (input/textarea font-size: max(16px, 1em), top of this file) is a bare ELEMENT selector that the
     .form-control-sm CLASS outranks — so iOS Safari auto-zoomed on focus, and a position:fixed sheet sits on
     the LAYOUT viewport, which iOS does not shrink with the zoomed VISUAL viewport (the same proven cause as
     the AI composer fix further down). 16px stops the zoom; the rest makes the sheet structurally unable
     to outgrow the viewport whatever is typed: border-box, flex children allowed to shrink, long words
     wrap, the timestamp may wrap, safe-area insets held. Desktop's inline row is untouched (lg+). */
  .mpa-note-panel {
    box-sizing: border-box; max-width: 100%; overflow-x: hidden;
    padding-left: max(1rem, env(safe-area-inset-left, 0px));
    padding-right: max(1rem, env(safe-area-inset-right, 0px));
    padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  .mpa-note-panel textarea.form-control { font-size: 16px; width: 100%; max-width: 100%; min-width: 0; box-sizing: border-box; }
  .mpa-note-panel .d-flex > * { min-width: 0; }
  .mpa-note-panel .form-label, .mpa-note-panel .form-text, .mpa-note-panel strong { overflow-wrap: anywhere; }
  .mpa-note-panel .text-nowrap { white-space: normal !important; }
}

@media (max-width: 991.98px) {
  /* Tabs scroll horizontally in one row; the active tab is scrolled into
     view by mpaTimetableTabs (init/pick). Same-specificity override —
     cascade order (this rule is later in the file) makes it win. */
  .mpa-tabs { display: flex; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; max-width: 100%; }
  .mpa-tabs::-webkit-scrollbar { display: none; }
  .mpa-tabs .nav-link { flex: 0 0 auto; white-space: nowrap; }

  /* Slot drawer goes full-screen (§18.4 S5), safe areas held. The panel
     identity header (panel.html .mpa-panel-head) sticks to the top of the
     ONE body scroll surface — negative margins pull it over the body's
     own padding so content scrolls under it, not visibly past a gap.
     No sticky footer (24 July 2026 ruling) — actions stay in the body. */
  /* Edge-pinned (S5 fix 5) and capped at 100% of the viewport — never
     100vw, which can include the scrollbar and push the page into
     horizontal scroll (round-3 fix 1). left+right:0 with width:auto
     spans the visible viewport exactly; overflow-x on the body rule
     above kills any content-driven sideways scroll. */
  #slot-drawer { left: 0; right: 0; width: auto; max-width: 100%; overflow-x: hidden; border-left: 0; }
  #slot-drawer .offcanvas-header { padding-top: calc(1rem + env(safe-area-inset-top, 0px)); }
  /* Sticky-header gap fix (11 Aug 2026): a NEGATIVE margin-top on the
     sticky child does NOT reliably cancel the scroll container's own
     top padding for sticky-offset purposes — verified live: it left a
     persistent ~16px gap between the container's real top edge and
     where the header actually rendered once stuck, regardless of scroll
     position, with content visibly scrolling through it. The robust
     fix moves the top padding off the CONTAINER instead of fighting it
     with a margin on the sticky element — padding-top:0 here, restored
     as .mpa-panel-head's own padding-top below. Left/right stay exactly
     as before (negative margin cancelling padding works fine there —
     only the sticky top-offset calculation was the problem). */
  #slot-drawer .offcanvas-body {
    padding-top: 0;
    padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  #slot-drawer .mpa-panel-head {
    position: sticky; top: 0; z-index: 5;
    margin-left: -1rem; margin-right: -1rem;
    /* density pass (9 Oct 2026): .6rem, not 1rem — the drawer header's own bottom padding and
       hairline already separate it; 1rem made title-from-header 33px */
    padding-top: .6rem; padding-left: 1rem; padding-right: 1rem;
    background: var(--mpa-surface);
  }
}

/* Quick-action chip — icon tile + label as a compact card button */
.mpa-action-btn {
  display: inline-flex; align-items: center; gap: .55rem;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: 12px; padding: .45rem .95rem .45rem .5rem;
  font-size: var(--mpa-text-md); font-weight: 600; color: var(--mpa-ink); text-decoration: none;
  box-shadow: var(--mpa-shadow);
  transition: border-color .15s ease, color .15s ease, transform .15s ease;
}
.mpa-action-btn:hover {
  border-color: var(--mpa-action); color: var(--mpa-ink-brand); transform: translateY(-1px);
}

/* Chips / badges */
.mpa-chip {
  display: inline-flex; align-items: center; gap: .3rem; border-radius: 999px;
  --mpa-chip-pad-y: .18rem;
  padding: var(--mpa-chip-pad-y) .6rem; font-size: var(--mpa-text-xs); font-weight: 700; white-space: nowrap;
  letter-spacing: .02em;
  /* Text alignment rule B (PROJECT_CONTEXT §9.3.1 rule 17, 26 Oct 2026):
     an inline badge lines up by its TEXT -- an inline-flex chip's baseline
     IS its text's baseline, so `baseline` puts the chip's text on the
     adjacent text's baseline. (Polish V2 §1e had used `middle`, which
     centres the BOX and left the chip's text ~1.1-1.4px below the line.)
     No effect where a chip is a flex/grid item (most placements). */
  vertical-align: baseline;
}
/* gap (4 Aug 2026): an icon + text chip (e.g. "Submitted" with a
   check-circle) had no space between them -- inline-flex collapses the
   HTML whitespace between an <i> and its following text node, same root
   cause .mpa-card-title already guards against with its own gap. Applies
   to every chip in the app, not just the one that surfaced it. */
.mpa-chip.brand { background: var(--mpa-tint); color: var(--mpa-ink-brand); }
.mpa-chip.ok { background: var(--mpa-ok-bg); color: var(--mpa-ok-ink); }
.mpa-chip.warn { background: var(--mpa-warn-bg); color: var(--mpa-warn-ink); }
.mpa-chip.danger { background: var(--mpa-danger-bg); color: var(--mpa-danger-ink); }
.mpa-chip.info { background: var(--mpa-info-bg); color: var(--mpa-info-ink); }

/* A non-interactive .mpa-row (a <div>, not a link — Email Templates'
   "Sent by other services" / "Not sent yet" informational rows, §61C):
   suppress the hover tint AND the pointer cursor .mpa-row's base rule
   doesn't set but a <div> can still inherit from an ancestor — belt and
   braces after Rory reported trying to click these (27 Aug 2026 final
   corrective): no hover, no chevron (simply omitted in the markup), a
   plain cursor at rest too, not just on hover. */
.mpa-row.static { cursor: default; }
.mpa-row.static:hover { background: transparent; cursor: default; }

/* Email Templates friendly editor (§61C, 26 Aug 2026) — a row standing in
   for content the app inserts automatically (a button, booking details)
   so an admin never sees the raw <p>/{token} markup behind it. */
.mpa-locked-chip {
  display: flex; align-items: center; gap: .5rem;
  background: var(--mpa-bg); border: 1px dashed var(--mpa-line);
  border-radius: var(--mpa-radius-sm); padding: .55rem .8rem;
  font-size: var(--mpa-text-md); color: var(--mpa-muted);
}
.mpa-locked-chip i { color: var(--mpa-ink-brand); flex-shrink: 0; }
.mpa-chip.neutral { background: var(--mpa-neutral-bg); color: var(--mpa-neutral-ink); }
/* Recessive count chip — a secondary detail sitting beside a primary
   one (Venues' slot count next to its class count). Fills like every
   other chip rather than using an outline, so the row reads as one
   family; the base .mpa-chip has NO background, so a variant-less
   chip renders as bare text and must not be used on its own. */
.mpa-chip.quiet { background: var(--mpa-hairline); color: var(--mpa-muted); }
/* Compact row action (Staff Time Off, 20 Aug 2026) — a small button that
   sits NEXT TO .mpa-chip status pills (Cancel beside "Pending", Withdraw
   beside "Approved") without towering over them: same vertical rhythm,
   font scale and pill radius as the chip, but still visibly a button
   (border + Bootstrap variant colours). Height ≈ chip height by
   construction (same padding/font/line-height). */
.mpa-btn-compact {
  padding: .18rem .6rem !important;
  font-size: var(--mpa-text-xs) !important;
  font-weight: 600;
  line-height: 1.35 !important;
  border-radius: 999px !important;
}
.mpa-chip.ink { background: var(--mpa-ink); color: #fff; }
.mpa-chip.trial { background: var(--mpa-trial-bg); color: var(--mpa-trial-ink); }

/* Semantic text utilities — token-true replacements for Bootstrap's
   text-success/text-danger where deltas and rates need colour */
.mpa-text-ok { color: var(--mpa-ok) !important; }
.mpa-text-warn { color: var(--mpa-warn) !important; }
.mpa-text-danger { color: var(--mpa-danger) !important; }
/* Warning-state TEXT that must be read (status notes, not icons or deltas):
   the -ink shade, never Bootstrap's .text-warning yellow (1.6:1 on white). */
.mpa-text-warn-ink { color: var(--mpa-warn-ink) !important; }

/* Soft alerts (semantic, product-controlled) */
.mpa-alert { border-radius: 12px; border: 1px solid transparent; font-size: var(--mpa-text-md); padding: .75rem 1rem; }
.mpa-alert.info { background: var(--mpa-info-bg); color: var(--mpa-info-ink); }
.mpa-alert.warn { background: var(--mpa-warn-bg); color: var(--mpa-warn-ink); }
.mpa-alert.danger { background: var(--mpa-danger-bg); color: var(--mpa-danger-ink); }
.mpa-alert.ok { background: var(--mpa-ok-bg); color: var(--mpa-ok-ink); }

/* Insight card — one plain-English sentence with the numbers in bold.
   Insights lead, tables support (Reports information hierarchy). */
/* Final Forensic Corrective Pass (4 Sep 2026, §6): align-items:center, not
   flex-start. Measured before the fix at 1440px: the 30px icon tile sat
   5.4px BELOW the single-line sentence's centre and the text 5.1px above
   the card's own centre (flex-start pinned the text to the top while the
   icon's .05rem margin-top pushed it down) -- Rory's "text sits too
   high/low relative to the icon tile" on every Reports tab. Centring the
   flex row puts icon and text block on one centre line whether the
   sentence takes one line or three, and the block itself centres inside
   the grid-cell-stretched card. No fixed heights anywhere, so longer copy
   still grows the card. */
.mpa-insight {
  display: flex; align-items: center; gap: .7rem; height: 100%;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius); box-shadow: var(--mpa-shadow);
  padding: .9rem 1rem; font-size: var(--mpa-text-md); color: var(--mpa-ink); line-height: 1.45;
}
/* ============================================================
   AI GENERATION BUSY STATE (CP-V2A corrective, 23 Aug 2026)
   ============================================================
   Rory clicked Regenerate several times during acceptance because the
   page told him nothing was happening, and every click was a real paid
   Anthropic request. Report Cards' existing pattern (hx-disabled-elt +
   an hx-indicator spinner) is the right shape but it leaves the button's
   LABEL unchanged, which on a long page reads as "nothing happened".

   These three helpers add the missing half — the label swaps to plain
   English and the whole target visibly goes to work — using htmx's own
   `htmx-request` class, so there is no bespoke JS state to get out of
   sync with the actual request.

     .mpa-gen-busy    on the element named by hx-indicator: makes it inert
                      for the duration (belt-and-braces with
                      hx-disabled-elt, which only covers the button) and
                      dims it. ⚠ The INERT half is the load-bearing part
                      and is browser-verified; the opacity dim is a
                      progressive enhancement that was observed NOT to
                      take effect in one Chrome profile during acceptance
                      (an inline opacity on the same element was ignored
                      too, so this is environmental rather than a cascade
                      problem). Never rely on the dim alone to tell a user
                      something is happening — the label swap below does
                      that job, and it is what was verified.
     .mpa-gen-idle    the normal label. Hidden while the request runs.
     .mpa-gen-working the "Regenerating this session..." label. Shown only
                      while it runs.

   The SERVER remains the authority: services/ai_guard.py refuses a second
   concurrent generation of the same object regardless of what any of this
   CSS does. This is feedback, not a lock. ============================ */
.mpa-gen-working { display: none; }
.htmx-request .mpa-gen-idle,
.htmx-request.mpa-gen-idle { display: none; }
.htmx-request .mpa-gen-working,
.htmx-request.mpa-gen-working { display: inline-flex; align-items: center; gap: .35rem; }
.mpa-gen-busy.htmx-request { opacity: .55; pointer-events: none; transition: opacity .15s; }

/* ⚠ .mpa-insight's `height: 100%` above is DESIGNED FOR A GRID CELL. Every
   original use is inside a `.row > .col-md-6.col-xl-4` (reports/index.html),
   where 100% is what makes a row of insight cards equal-height — that is the
   whole point of the declaration and it must not be removed.

   It is a TRAP anywhere else. As a direct child of `main.main-content` the
   percentage resolves against main's full height, so a one-line explainer
   inflates to whatever the page is tall. Found in CP-V2A acceptance (23 Aug
   2026), measured in the browser on /class-plans: the insight rendered
   **4,515px** tall under an 80-row table and took the document from 4,665px
   to **9,018px** — roughly 4,400px of blank nothing below the list, which
   read as "the page ended" and hid the classes above it.

   `.mpa-insight-block` is the opt-in escape for a standalone, full-width
   insight that has no siblings to line up with. Deliberately a modifier and
   not a change to the base rule: the base behaviour is correct where it is
   already used, and a feature branch must not silently reflow Reports. */
.mpa-insight.mpa-insight-block { height: auto; }
/* (§6) the old .05rem margin-top on the tile was the second half of the
   optical offset above -- gone; the flex centring handles it. */
.mpa-insight .mpa-icon-tile.sm { margin-top: 0; }
.mpa-insight > span:not(.mpa-icon-tile) { min-width: 0; }
.mpa-insight.warn .mpa-icon-tile.sm { background: var(--mpa-warn-bg); color: var(--mpa-warn); }
.mpa-insight.danger .mpa-icon-tile.sm { background: var(--mpa-danger-bg); color: var(--mpa-danger); }
.mpa-insight.ok .mpa-icon-tile.sm { background: var(--mpa-ok-bg); color: var(--mpa-ok); }

/* Chart container inside a section card */
.mpa-chart-box { position: relative; padding: .5rem 1.25rem 1.25rem; }

/* Inline table micro-viz: share-of-max bar + small rate meter.
   Pure CSS — the printed value stays in the cell, the bar just adds shape. */
.mpa-share-bar {
  width: 72px; height: 3px; border-radius: 999px; overflow: hidden;
  background: var(--mpa-hairline); margin-left: auto; margin-top: .3rem;
}
.mpa-share-bar span { display: block; height: 100%; background: var(--mpa-action); border-radius: 999px; }
.mpa-rate-meter {
  display: inline-block; width: 56px; height: 4px; border-radius: 999px; overflow: hidden;
  background: var(--mpa-tint-strong); vertical-align: middle;
}
.mpa-rate-meter span { display: block; height: 100%; background: var(--mpa-action); border-radius: 999px; }
.mpa-rate-meter.low { background: #f8ecd9; }
.mpa-rate-meter.low span { background: var(--mpa-warn); }

/* Empty state */
.mpa-empty { padding: 1.5rem 1.25rem 1.7rem; text-align: center; color: var(--mpa-muted); font-size: var(--mpa-text-sm); }
.mpa-empty .mpa-empty-ico {
  width: 40px; height: 40px; border-radius: 50%; margin: 0 auto .6rem;
  display: grid; place-items: center; font-size: var(--mpa-text-lg);
  background: var(--mpa-hairline); color: var(--mpa-muted);
}
.mpa-empty.good .mpa-empty-ico { background: var(--mpa-ok-bg); color: var(--mpa-ok); }
/* Density pass (9 Oct 2026): an empty state INSIDE a padded card body must not add a second full set of
   padding (nested padding: the body's .35rem/1.1rem + the empty state's 1.5rem/1.7rem left ~120px of blank
   card for one line, e.g. Student → Siblings). The body already provides the inset. A p-0 body keeps it. */
.mpa-card-body:not(.p-0) > .mpa-empty { padding: .6rem 0 .2rem; }

/* Loading state — shimmer skeleton block */
.mpa-skeleton {
  border-radius: var(--mpa-radius-sm);
  background: linear-gradient(90deg, var(--mpa-hairline) 25%, var(--mpa-track) 42%, var(--mpa-hairline) 60%);
  background-size: 300% 100%;
  animation: mpa-shimmer 1.4s ease infinite;
  min-height: 1rem;
}
@keyframes mpa-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .mpa-skeleton { animation: none; } }

/* OPS-1 FINAL MICRO-POLISH PASS (Part 3) — the parent "Changed classes"
   click-through's brief highlight on the actual occurrence card/row it
   scrolled to. A ring, not a colour change: safe against the venue-
   colour system (which already owns each card's background) and safe
   against any text-contrast pairing already in play on that card. */
.mpa-focus-flash {
  animation: mpa-focus-flash 2.4s ease-out 1;
  border-radius: var(--mpa-radius-sm, 6px);
}
@keyframes mpa-focus-flash {
  0%, 40% { box-shadow: 0 0 0 3px var(--mpa-action, #6d4fe0); }
  100% { box-shadow: 0 0 0 3px rgba(0,0,0,0); }
}
@media (prefers-reduced-motion: reduce) {
  .mpa-focus-flash { animation: none; box-shadow: 0 0 0 3px var(--mpa-action, #6d4fe0); }
}

/* Form section card + sticky save bar (forms groundwork, applied later) */
.mpa-form-card .mpa-card-body { padding-top: .75rem; }
.mpa-savebar {
  position: sticky; bottom: 0; z-index: 10;
  display: flex; align-items: center; gap: .6rem; justify-content: flex-end;
  background: rgba(255,255,255,.92); backdrop-filter: blur(6px);
  border-top: 1px solid var(--mpa-line);
  padding: .7rem 1.25rem; margin-top: 1rem;
}

/* List rows inside cards (activity/action lists) */
.mpa-rows { padding-bottom: .4rem; }
/* ── An explanatory line INSIDE a .mpa-rows list (Failed emails' "Invoice
   emails — parent never received..."). Dashboard fix, 21 Sep 2026.
   It must share .mpa-row's own 1.25rem inset or it reads as misaligned
   against the very rows it introduces -- which is exactly what Rory saw.
   Written against .mpa-rows .mpa-stat-note rather than a one-off class so
   every note in every rows-list is right, now and later. */
.mpa-rows .mpa-stat-note {
  padding: .55rem 1.25rem .45rem;
  margin-top: 0;
  line-height: 1.45;
}
/* A SECOND explanatory group further down the same list (Report cards,
   under the invoice rows) earns a divider and a little more air, so the
   two groups read apart instead of running together. */
.mpa-rows .mpa-row + .mpa-stat-note {
  border-top: 1px solid var(--mpa-hairline);
  padding-top: .8rem;
}
.mpa-row {
  display: flex; align-items: center; gap: .8rem;
  padding: .62rem 1.25rem; text-decoration: none; color: var(--mpa-ink);
  font-size: var(--mpa-text-md); border-top: 1px solid var(--mpa-hairline);
  transition: background .12s ease;
}
.mpa-row:hover { background: var(--mpa-row-hover); color: var(--mpa-ink); }
/* A short control strip (filter chips) in a card body directly above a
   .mpa-rows list (Settings -> Emails "Recent emails", 19 Oct 2026). With
   pb-0 the chips sat ON the first row's border-top, so the band above the
   first row read as crushed while every later divider had a row's
   padding on both sides. The strip now ends one .mpa-row padding above
   that divider -- the same .62rem each row keeps inside its own border.
   (On Settings pages >= 992px the Settings card layer's --set-pad-bottom,
   .75rem, governs every card body's bottom and wins here: same intent.) */
.mpa-card-body.mpa-rows-lead { padding-bottom: .62rem; }
.mpa-row-main { font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mpa-row-side { color: var(--mpa-muted); font-size: var(--mpa-text-sm); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; display: inline-flex; align-items: center; gap: .45rem; }
.mpa-row-side.danger { color: var(--mpa-danger); font-weight: 600; }
/* Time Off card (Rory acceptance fix, 27 Sep 2026): "Date passed" sits
   in a right-flush .mpa-row-side alongside the date text. Because the
   date's own rendered width varies (day-name + day-of-month, a
   proportional font — e.g. "26 Aug" is wider than "2 Sep"), the whole
   side box's width varied row to row, and since it's anchored by its
   RIGHT edge, that shifted the badge's LEFT edge sideways — never one
   specific date, any two different-width dates disagree. A fixed-width,
   right-aligned date column removes the variance: the date's own right
   edge is now constant, so the badge (whose own text is always fixed —
   "Date passed") lines up in one column too. Scoped to this one date
   slot; no other .mpa-row-side usage is affected. */
.mpa-timeoff-date { display: inline-block; width: 4.75rem; text-align: right; }

/* Accounting software selector (partials/_accounting_choice_form.html):
   question, control and Save share one flex row and wrap naturally when
   narrow. A comfortable minimum, capped at the row so a 390px screen never
   scrolls sideways. */
.mpa-accounting-choice { min-width: 15rem; max-width: 100%; }
/* Credit/discount row (parent invoice — final visual corrective B3,
   25 Aug 2026): the EXISTING success semantics as a subtle full-row
   tint, layered on the green text those rows already carry. A wash,
   not a banner — same token the ok-chip background uses. */
.mpa-row.mpa-row-credit { background: var(--mpa-ok-bg); }
.mpa-row.mpa-row-credit:hover { background: var(--mpa-ok-bg); }
/* Parent invoice hero card (final visual corrective rounds 4+5, 25 Aug
   2026). Root cause of the "amount floats near the edge" report:
   .mpa-card-body's asymmetric padding (top .35rem — designed to follow a
   card-head this hero doesn't have) crowded the £ figure against the top
   edge at every width. Balanced padding + the kpi component's 1rem
   icon/text gap (gap-3 in the template) make the icon, amount/identity
   stack and status chip read as one deliberately-composed component. */
.mpa-card-body.mpa-inv-summary { padding-top: 1.15rem; }
/* GENERIC version of the same fix, for any .mpa-card-body used as the
   ONLY child of its .mpa-card (no .mpa-card-head above it) — the
   .35rem top inset above exists purely to avoid double-spacing under a
   head's own bottom padding, and starves a headless card's content
   against its top border otherwise. Email Templates' editor meta card
   ("Who receives it" / "When it's sent" / ...), §61 final visual
   corrective, 27 Aug 2026, hit the exact same root cause .mpa-inv-summary
   was named for — same fix, generically named so the NEXT headless card
   doesn't reinvent it a third time. */
/* Setup-banner corrective (1 Oct 2026): a headless card body is inset
   EQUALLY top and bottom at every breakpoint (the mobile .mpa-card-body
   rule's .85rem bottom used to leave standalone cards top-heavy), so a
   single-row card's content sits vertically centred in it. */
.mpa-card-body.standalone { padding-top: 1.15rem; padding-bottom: 1.15rem; }
/* Guided Setup V1 -- final responsive layout polish (7 Sep 2026). Below
   md, a completed card's action row stops being natural-width pills
   left-floating in whatever space is left and becomes a real grid,
   aligned to the same content column every other row uses.
   `.mpa-setup-actions`: 2-up equal-width/height buttons (Venue/Teacher/
   Pupil "View X" + "Add X"; Bulk upload, already hidden below md via its
   own d-none d-md-inline-block, is simply excluded from the grid flow --
   never leaves a gap since display:none removes a grid item entirely).
   `.mpa-action-full` on one item (Classes' "Schedule on timetable")
   forces it onto its own full-width row regardless of auto-placement
   order. `.mpa-setup-actions-stack`: Billing's two review buttons, full
   width, stacked single-column (not a 2-up grid -- their labels are too
   long to share a row legibly). */
@media (max-width: 767.98px) {
  .mpa-setup-actions-wrap { width: 100%; }
  .mpa-setup-actions { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; width: 100%; }
  .mpa-setup-actions .btn { width: 100%; }
  .mpa-setup-actions .mpa-action-full { grid-column: 1 / -1; }
  .mpa-setup-actions-stack { display: flex; flex-direction: column; gap: .5rem; width: 100%; }
  .mpa-setup-actions-stack .btn { width: 100%; }
}
/* The OPPOSITE case from .standalone above, same root cause read the
   other way: a .mpa-card-body whose content is a LEDE — a short
   explanatory line acting as the card-head's own subtitle (Email
   Templates' "Sent by other services" / "Not sent automatically yet"
   cards, §61 micro-corrective, 27 Aug 2026) — not generic body content a
   reader expects a fresh .35rem paragraph-break before. Root cause of
   "the heading and its explanation read as unrelated blocks": the
   DEFAULT .mpa-card-body top padding (.35rem) stacked on top of
   .mpa-card-head's own BOTTOM padding (.75rem) already gives an
   ordinary body ~1.1rem of head-clearance — correct for body content
   that deserves to feel like a new section, wrong for a lede that
   should read as part of the heading, the way .mpa-page-sub sits close
   under .mpa-page-title elsewhere in the app. Zeroing padding-top
   leaves ONLY the head's own .75rem as the gap — visibly tighter, and
   still never zero (so the lede doesn't touch the head outright). Pair
   with Bootstrap's `pb-2` (not `pb-0`) on the same element for the
   brief's other half — a LITTLE more room before the rows below, where
   .pb-0 previously gave literally none. */
.mpa-card-body.lede { padding-top: 0; }
/* Phone: the ms-auto status chip was stranded — pushed hard right on
   whatever wrap line it landed on, disconnected from the figures it
   qualifies. It becomes its own full row, indented to the £ figure's own
   left edge (past the 48px icon tile + the row's 1rem gap), so the card
   reads top-to-bottom: £ figure → label → term → status. Desktop keeps
   the chip top-right. */
@media (max-width: 575.98px) {
  .mpa-inv-summary > .ms-auto {
    flex-basis: 100%;
    margin-left: calc(48px + 1rem) !important;
  }
}
/* .mpa-stat-note carries margin-top for its stat-card use; as a direct
   child of .mpa-row it's a blockified flex item, so that margin would
   inflate the row (portal invoice subtotal row) — neutralize it here. */
.mpa-row > .mpa-stat-note { margin-top: 0; }
/* Sentence-shaped row content (portal notices): wrap instead of the
   list-row ellipsis — a notice cut mid-sentence loses its meaning at
   phone width (W5 mobile pass, 2 Aug 2026). */
.mpa-row-main.wrap { white-space: normal; overflow: visible; }
/* §4 (29 Aug 2026, FINAL FINAL acceptance corrective) — Class Detail
   Pupils This Term: a small sticky column header naming the bare "%"
   values. position:sticky (not fixed/JS) so it rides the page's own
   scroll and simply stops sticking once .mpa-card (its containing block
   — overflow:hidden already makes it one, same mechanism Timetable's
   grid headers rely on) scrolls past; no nested overflow:auto anywhere.
   .mpa-pupils-attn-col is shared between the header's "Attendance" label
   and every row's percentage so both align into one visual column. */
.mpa-pupils-col-header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: .4rem 1.25rem;
  background: var(--mpa-surface);
  border-top: 1px solid var(--mpa-hairline);
  border-bottom: 1px solid var(--mpa-hairline);
  font-size: var(--mpa-text-xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--mpa-muted);
}
.mpa-pupils-attn-col { min-width: 4.2rem; text-align: right; justify-content: flex-end; }
.mpa-row-chev {
  color: var(--mpa-neutral); font-size: var(--mpa-text-sm); opacity: 0; transform: translateX(-4px);
  transition: opacity .12s ease, transform .12s ease;
}
.mpa-row:hover .mpa-row-chev { opacity: 1; transform: none; }
/* Teacher Dashboard polish (7 Aug 2026, Rory) — Upcoming events on mobile:
   .mpa-row-main's flex:1/min-width:0 squeezed a long event title into a
   narrow column beside the nowrap date/time/location side text, wrapping
   the title across 2-3 cramped lines. mpa-row-stack-sm is a MOBILE-ONLY
   modifier (opt-in class, only applied on this one row) that stacks the
   title onto its own full-width row and drops the date/time/location to a
   second row beneath it. Scoped to max-width:767.98px (this app's
   standing mobile breakpoint) and to rows carrying the modifier class
   only, so .mpa-row's shared desktop behaviour — and every OTHER card
   using plain .mpa-row elsewhere in the app — is completely untouched. */
@media (max-width: 767.98px) {
  .mpa-row.mpa-row-stack-sm { flex-wrap: wrap; }
  .mpa-row.mpa-row-stack-sm .mpa-row-main { flex: 1 1 100%; }
  .mpa-row.mpa-row-stack-sm .mpa-row-side { flex: 1 1 100%; }
}

/* .mpa-row-stack — the UNCONDITIONAL (every width) sibling of
   .mpa-row-stack-sm above, added 29 Aug 2026 (Rory FINAL acceptance
   corrective, §15/16/17): several list/card-row replacements for old
   desktop Excel-style tables (Staff Performance Summary's per-class
   breakdown, etc.) now use .mpa-row-main.wrap at DESKTOP widths too —
   with no separate desktop table to fall back to, a genuinely long class
   name wrapping to 2 lines could overlap a plain .mpa-row-side amount on
   desktop the exact same way it could on mobile (the bug .mpa-row-stack-sm
   was built to prevent). Same mechanism, just not gated to <768px. */
.mpa-row.mpa-row-stack { flex-wrap: wrap; }
.mpa-row.mpa-row-stack .mpa-row-main { flex: 1 1 100%; }
.mpa-row.mpa-row-stack .mpa-row-side { flex: 1 1 100%; }

/* Section label — quiet uppercase divider between page zones */
/* UI-CONSISTENCY-1B: the section rhythm tokens (was 1.7rem 0 .8rem -- the
   'double gap' Rory saw above every section heading). */
.mpa-section { display: flex; align-items: center; gap: .55rem; margin: var(--mpa-space-section) 0 var(--mpa-space-label); }
.mpa-section:first-of-type { margin-top: 0; }
.mpa-section-label {
  font-size: var(--mpa-text-xs); font-weight: 800; letter-spacing: .11em; text-transform: uppercase;
  color: var(--mpa-muted); margin: 0;
}

/* Dashboard header corrective (Rory, 10 Sep 2026): the current-term date
   range now sits on the SAME row as "Needs attention" (heading+count
   left, date right) instead of an empty second line under the term pill
   in .mpa-page-head. Dashboard-only — .mpa-section itself (every other
   page's section dividers) stays byte-identical; this is an additive
   modifier class, never a redefinition. justify-content:space-between
   does the left/right split; flex-wrap lets the date range drop to its
   own line under narrow widths instead of clipping/overlapping, the same
   "sensible stacking" every other .mpa-page-head row already gets. */
.mpa-dashboard-attention-row { justify-content: space-between; flex-wrap: wrap; row-gap: .3rem; }
.mpa-dashboard-attention-label { display: flex; align-items: center; gap: .55rem; }
.mpa-dashboard-term-range { margin: 0; }

/* Muted KPI variant (metric awaiting data — never fake a value) */
.mpa-kpi.muted .mpa-kpi-value { color: var(--mpa-muted); }
.mpa-kpi.muted .mpa-icon-tile { background: var(--mpa-hairline); color: var(--mpa-neutral); }

/* Big standalone figure inside a card (ageing buckets, money splits) */
.mpa-stat { font-size: var(--mpa-text-xl); font-weight: 800; line-height: 1.15; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
/* Secondary figure sitting beneath a headline .mpa-stat (Invoices summary:
   "Total invoiced" owns its row, "Paid so far"/"Still to collect" sit
   smaller under it — Rory, 3 Aug 2026). */
.mpa-stat.sm { font-size: 1.15rem; }
/* A status WORD sitting in a stat slot ("Reconnect required", "Not
   connected", "Waiting to sync" on the Dashboard's accounting card): panel-
   title scale, not a headline figure — an actionable warning, not an alarm
   (1 Oct 2026). Beats the phone .mpa-stat step-down by specificity. */
.mpa-stat.status { font-size: var(--mpa-text-lg); font-weight: 700; letter-spacing: 0; line-height: 1.3; }
.mpa-stat-label { font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase; color: var(--mpa-muted); margin-top: .2rem; }
.mpa-stat-note { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin-top: .9rem; }
/* §2 (29 Aug 2026, FINAL FINAL corrective): Invoice list's explanatory
   line was "tiny subordinate copy" per Rory — one step up in size/colour
   from the default note, still explicitly NOT a headline (no weight
   change, no stat-sized figure). Opt-in modifier so every other
   .mpa-stat-note (Dashboard, Reports) is untouched. */
.mpa-stat-note.prominent { font-size: var(--mpa-text-sm); color: var(--mpa-ink); }

/* Dot-keyed status figures + thin stacked meter (invoice pipeline etc.) */
.mpa-status-list { display: flex; gap: 1.6rem; flex-wrap: wrap; }
.mpa-status-count { font-size: 1.25rem; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; }
.mpa-status-name {
  display: flex; align-items: center; gap: .4rem; margin-top: .25rem;
  font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase; color: var(--mpa-muted);
}
.mpa-status-dot { width: .5rem; height: .5rem; border-radius: 50%; flex-shrink: 0; }
.mpa-meter {
  display: flex; height: 6px; border-radius: 999px; overflow: hidden;
  background: var(--mpa-hairline); margin-top: 1.15rem;
}
.mpa-meter span { height: 100%; }
.mpa-meter span + span { border-left: 2px solid var(--mpa-surface); }

/* ============================================================
   BACK TO TOP — bottom-LEFT, mirroring the AI Assistant's bottom-RIGHT
   position (§9 Stage B2 refinement). Previously pinned at a hardcoded
   left:236px, calibrated to clear the OLD persistent 220px sidebar —
   that sidebar is gone (replaced by the hamburger drawer), so the button
   floated with no visual anchor. Anchoring both floating controls to
   opposite bottom corners at the same 1.5rem inset makes the pairing
   read as one deliberate system and guarantees they can never overlap.
   z-index sits one tier below the AI Assistant (999) — see its own
   z-index comment below — so on the rare ultra-narrow viewport where
   they'd visually meet, the assistant (the primary control) wins.
   ============================================================ */
.mpa-back-to-top {
  position: fixed; bottom: 1.5rem; left: 1.5rem; z-index: 998;
  width: 2.2rem; height: 2.2rem; border-radius: 50%;
  background: var(--mpa-action-2); color: #fff; border: none;
  display: flex; align-items: center; justify-content: center; font-size: var(--mpa-text-base);
  box-shadow: var(--mpa-shadow);
  opacity: 0; transition: opacity .2s; pointer-events: none; cursor: pointer;
}

/* ============================================================
   AI ASSISTANT (moved from base.html; accent token in :root)
   ============================================================ */
.ask-claude-panel { display: flex; flex-direction: column; }
/* AI-SUPPORT-V1-A: the two hardcoded orange glows became tokens (same
   values) so the support plane's green launcher gets a matching glow from
   the same rule instead of needing a parallel modifier class. */
.ai-assistant-toggle { transition: transform .15s ease, box-shadow .15s ease; box-shadow: var(--ai-assistant-glow); font-family: var(--mpa-font); }
.ai-assistant-toggle:hover { transform: translateY(-1px); box-shadow: var(--ai-assistant-glow-hover); }
.ai-assistant-toggle-icon { margin-right: .4rem; }
.ai-assistant-panel { box-shadow: 0 12px 40px rgba(0,0,0,.18); border-color: rgba(0,0,0,.06) !important; }
.ai-assistant-send { transition: opacity .15s ease; flex-shrink: 0; }
.ai-assistant-send:disabled { opacity: .5; }

/* Composer polish (final micro-corrective, 11 Sep 2026): the textarea +
   send button should read as ONE control, not two joined fragments.
   Root cause of the stray seam: the textarea's `border-0` (base.html)
   only zeroes its BORDER width — the global `.form-control { border-
   radius: var(--mpa-radius-sm) }` rule (this file, ~L205) still applies
   its own radius to it, and Bootstrap's `.input-group > .form-control:
   not(:last-child)` zeroing rule only resets the two corners it expects
   to butt against a bordered sibling; with no border there to overlap,
   a sub-pixel gap can show at the shared edge (and at the group's own
   rounded-3 clip) at certain zoom levels. Force the radius flush and
   the button's margin to zero, and give the group an explicit solid
   background so any residual antialiasing sliver reads as the same
   white rather than a visible line. Scoped to this composer only —
   never touches the app-wide .form-control/.input-group rules other
   forms rely on. Focus state (the wrapper's plain .border, the
   textarea's own box-shadow:none) is untouched. */
.ai-assistant-composer .input-group { background: var(--mpa-surface); }
.ai-assistant-composer .input-group > .form-control { border-radius: 0; margin: 0; }
.ai-assistant-composer .input-group > .ai-assistant-send { margin: 0; }

/* Full-height send/stop area (final visual micro-fix, 11 Sep 2026, Part
   C): ROOT CAUSE of the white rectangle Rory kept flagging above the
   orange button — the input-group used to force `align-items:flex-end`
   (base.html), pinning this button to a fixed 2.5rem square at the
   BOTTOM of a row that is always taller than that (the textarea's own
   2-row/grown height), so the row's own white `background` above sat
   exposed for however much taller the textarea was. Fixed at the actual
   cause, not by hiding it: the input-group now uses flexbox's default
   `align-items:stretch` (no inline override left in base.html at all),
   and `height:auto` here makes the button fill exactly that stretched
   height — one continuous orange (or dark, mid-generation) block from
   the row's top edge to its bottom edge, whatever that height is.
   Textarea sizing/autoGrowTextarea() behaviour is completely untouched —
   this only changes how tall the BUTTON reads, never the row itself.
   Icon nudged very slightly larger (1.05em) so it doesn't look lost in
   the now-taller area. Both Send and Stop share this — same slot,
   swapped by x-show, must stay dimensionally identical. Scoped entirely
   to .ai-assistant-composer; no generic .btn/.input-group rule touched. */
.ai-assistant-composer .input-group > .ai-assistant-send {
  background: var(--ai-assistant-accent); color: #fff; border: none; border-radius: 0;
  width: 2.5rem; height: auto; align-self: stretch;
  display: flex; align-items: center; justify-content: center;
}
.ai-assistant-composer .input-group > .ai-assistant-send.ai-assistant-stop {
  background: var(--mpa-ink);
}
.ai-assistant-composer .input-group > .ai-assistant-send i { font-size: 1.05em; }

/* Mobile AI Assistant Drawer + Viewport Fix V1 (2 Sep 2026) — supersedes
   the AI CAPABILITY V1A 480px corrective below 480px (whose intent —
   never let the panel's left edge go negative — this fully subsumes).

   ROOT CAUSE of the physical-iPhone reports (composer/send clipping,
   document wider than the viewport, poor keyboard behaviour) traced by
   reading base.html + this file:
     1. The V1A corrective kept the DESKTOP right-anchored floating-window
        positioning object (Alpine's :style: width/right/bottom/height)
        and patched it with @media(max-width:480px) !important overrides
        stacked on top. Two competing sources of truth for the same box
        is exactly the kind of thing that reads fine in an emulator
        (Chromium's viewport math is forgiving) and still misbehaves on
        real WebKit, which is markedly less forgiving about elements
        computed near/at the viewport edge. base.html now empties that
        :style object entirely below the breakpoint (mobile ? {} : ...)
        so THIS block is the only source of truth on mobile — no
        !important fights, nothing left for a real device to disagree
        with.
     2. 480px never matched this app's own mobile contract. Every other
        full-screen mobile surface (Density V3, #slot-drawer, the nav
        offcanvas — .brand-nav above) switches at 991.98px. A panel that
        only got corrected below 480px still ran the cramped desktop
        floating-window math on anything from a large phone / small
        tablet up to 991px, unlike everywhere else in the app.
     3. Unlike #slot-drawer (timetable/index.html), the assistant never
        locked background scroll on open or reset it on close — nothing
        to reset means whatever horizontal-overflow state the underlying
        page happened to be in just showed through, matching Rory's
        "can remain wider than the viewport after closing" report. Fixed
        in askClaude()'s openPanel()/closePanel() (base.html), reusing
        #slot-drawer's own documented defensive rationale: an already-
        marginally-overflowing background page can drag an open overlay
        sideways on WebKit even though the overlay's own box is bounded.
     4. AI responses can render fenced code blocks (marked.js -> <pre>
        <code>), which — unlike tables (.tbl-wrap below, already fixed) —
        had no wrap/scroll rule of their own; a long unwrapped line was
        silently clipped (not scrollable) inside the panel's own
        overflow:hidden. Now contained + independently scrollable,
        closing the same class of bug tables already had fixed.

   FIX: a genuine full-screen drawer (inset:0, 100dvh — dvh already
   relied on by this app, see .brand-nav's mobile sizing elsewhere) using
   the SAME safe-area treatment just banked on .brand-nav .offcanvas-
   header / .btn-close (iOS PWA drawer-header safe-area fix V1) and the
   SAME visualViewport keyboard-height sync already proven on #slot-
   drawer — nothing new invented, this app's own established mobile
   patterns applied to the one surface that still lacked them. Desktop/
   tablet (>=992px) keep the original expanded/compact positioning math
   completely untouched — this block only exists below the breakpoint.
   Positioning/sizing (top/left/right/bottom/width/height) need no
   !important — base.html's :style is emptied on mobile, so there's no
   inline value left to compete with. border-radius still needs it (the
   one property base.html's STATIC style="" attribute still sets), and so
   do the header/composer padding overrides below (Bootstrap's px-3/py-3/
   p-2 spacing UTILITIES are !important by design, unlike a plain
   component class such as .offcanvas-header) — both found by actually
   running the QA suite against this block, not assumed. */
@media (max-width: 991.98px) {
  .ai-assistant-panel {
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    border-radius: 0 !important;
    /* Real-device corrective pass 2: the desktop floating-window's own
       .border utility class (base.html, on the SAME element -- a soft
       border makes sense around a card floating over page content) and
       its border-color/box-shadow (app.css, bare .ai-assistant-panel
       rule, no media query -- both !important) were still rendering on
       mobile, unscoped, which is the PROVEN cause of "still visually
       behaves like a bordered/contained white rectangle rather than a
       true edge-to-edge overlay": a full-viewport surface has nothing to
       visually separate itself FROM, so a border/shadow around its own
       edges reads as a gutter around a smaller rectangle floating inside
       the screen, not a true full-screen drawer. */
    border: none !important;
    box-shadow: none !important;
  }
  /* !important needed here too (found by running the QA suite, not
     assumed): the header carries Bootstrap's px-3/py-3 UTILITY classes,
     and Bootstrap's own spacing utilities are !important by design —
     unlike .brand-nav .offcanvas-header's plain (non-utility) component
     padding, which a same-specificity override could beat outright. */
  .ai-assistant-panel .ai-assistant-header {
    padding-top: calc(1.05rem + env(safe-area-inset-top, 0px)) !important;
    border-radius: 0 !important;
  }
  /* Same 44x44 iOS HIG floor + technique as .brand-nav .btn-close
     (drawer safe-area fix V1): grow the invisible hit box, leave the
     bi-x-lg glyph's own font-size untouched. */
  .ai-assistant-panel .ai-assistant-close {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* Same reasoning as the header above — .p-2 is also an !important
     Bootstrap utility. */
  .ai-assistant-panel .ai-assistant-composer {
    padding-bottom: calc(.5rem + env(safe-area-inset-bottom, 0px)) !important;
  }
  /* Real-device iOS forensic fix (amendment): the composer textarea sits
     in Bootstrap's .input-group-sm, which renders it at font-size:var(--mpa-text-md)
     (measured: 14px, confirmed with Playwright at 375/390/430 — the
     mobile media query never touched font-size, only padding/height
     elsewhere). Below 16px, iOS Safari auto-zooms + pans the ENTIRE page
     on focus — this is the PROVEN root cause of Rory's physical-iPhone
     report ("drawer disappears", "underlying app zoomed", "orange header
     overlaps the status bar", "close button no longer visible"): the
     panel is position:fixed against the LAYOUT viewport, which iOS never
     resizes for either the keyboard or an auto-zoom, while the VISUAL
     viewport (what's actually on screen) shrinks/pans — see
     syncMobileGeometry() (base.html) for the width/top/left tracking that
     now also runs defensively for a manual pinch-zoom, but the fix here
     is the one that stops iOS from ever zooming in the first place.
     Scoped to ONLY this textarea, ONLY below 992px — desktop's already-
     reviewed 14px composer and every other control app-wide are
     untouched (Rory's "do not globally enlarge the rest of Solis"). */
  .ai-assistant-panel .ai-assistant-composer textarea {
    font-size: 16px !important;
  }
  /* --ai-toggle-banner-offset (amendment, 2 Sep 2026): 0px normally: the
     pre-existing #mpa-install-banner (full-width, bottom:0, z-index:9999
     — from BEFORE this branch, untouched by it) and the toggle
     (z-index:999) both anchor bottom-right, so once a browser crosses the
     3rd-visit threshold that shows the banner, it sits ON TOP of and
     completely covers the toggle — proven with Playwright (identical
     stacking, full rect overlap) reproducing exactly what Rory saw:
     "no floating AI Assistant button". base.html's askClaude().init()
     sets this var to the banner's live height only while it's visible
     (MutationObserver on the banner's style attribute, since the banner
     script can set display asynchronously — the beforeinstallprompt path
     fires whenever Chrome decides to, not at page load), lifting the
     toggle clear above it. Desktop is unaffected by construction — this
     var is only read inside this mobile media query. */
  .ai-assistant-toggle {
    bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px) + var(--ai-toggle-banner-offset, 0px)) !important;
  }
  /* Real-device corrective pass 2 (closed-trigger occlusion): the desktop
     text pill (~130x44px, padding:.7rem 1.35rem + "AI Assistant" label)
     was proven to sit on top of real report/card content at phone
     widths -- KPI values, comparison "Change" columns, New Joiners rows,
     Interest card actions all measured underneath its bottom-right
     footprint. Collapses to a 52x52 icon-only circle instead (still
     clears the 44x44 iOS HIG tap-target floor used everywhere else in
     this app) -- title="AI Assistant" (base.html) keeps it labelled for
     accessibility/hover, the visible text just isn't needed to identify
     a chat-bubble-shaped FAB, which is this app's smallest possible
     footprint for a control that must stay reachable from every page. */
  .ai-assistant-toggle {
    width: 52px !important;
    height: 52px !important;
    padding: 0 !important;
    border-radius: 50% !important;
    /* display is NOT !important, deliberately -- Alpine's x-show="!open"
       (base.html) sets this same element's inline style.display to
       'none' while the panel is open and '' (falls back to this CSS
       value) while closed. An !important here would have permanently
       beaten Alpine's inline 'none', leaving the trigger visibly
       floating over the open full-screen panel -- caught by this
       branch's own QA (check 10, "floating trigger hidden while drawer
       is open"). The button's own inline style="" attribute never sets
       display at all, so nothing here needs !important to win. */
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .ai-assistant-toggle-label { display: none; }
  .ai-assistant-toggle-icon { margin-right: 0; font-size: 1.15rem; }

  /* Ask AI panel whitespace, mobile half (final micro-corrective, 11 Sep
     2026): the panel itself stays a genuine full-screen drawer here
     (height:100dvh above, matching every other mobile drawer in this
     app) — it does not shrink like the desktop floating panel does
     (base.html's :style object, non-expanded branch). Left alone, a
     short conversation's messages sit at the TOP of that tall area,
     leaving a large visible gap directly above the composer. Bottom-
     anchoring the transcript (only once it's non-empty — `has-messages`,
     set in base.html by `:class="messages.length > 0 ? ...`) removes
     that gap by having short content sit flush above the composer
     instead; a long conversation is unaffected, since once content
     fills the area there is no extra space left for justify-content to
     distribute and the existing overflow-y:auto + explicit
     scrollToBottom() calls behave exactly as before. */
  .ai-assistant-panel .ask-claude-messages.has-messages {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
  }
}
/* ============================================================
   AI RESPONSE TYPOGRAPHY (Typography + Composer Polish, 2 Sep 2026)
   Scoped ENTIRELY to .ask-claude-messages -- global h1-h6/p/ul/blockquote
   are untouched. Root cause of Rory's "feels like a separate document"
   report: renderMarkdown() (base.html) runs raw marked.js output through
   x-html with NO heading/paragraph/list/blockquote/hr/link rules of its
   own, so Claude's markdown fell back to the BROWSER'S default UA
   stylesheet -- literal default <h1>-<h6> sizes (2em/1.5em/1.17em on TOP
   of the message bubble's own font-size, i.e. a ~28px h1 inside a 14px
   conversation), default paragraph/list margins, etc. Only .table/.pre/
   .code had scoped rules (the two earlier drawer-fix passes). This closes
   the same gap for everything else marked.js can emit, one owned
   hierarchy regardless of which heading level Claude happens to use, so
   a response is never dramatically larger just because it emitted an h1.
   Base reading size is the pre-existing .875rem set inline on
   .ask-claude-messages itself (untouched) -- every size below is relative
   to that, using the app's own tokens (--mpa-font/--mpa-ink/--mpa-muted/
   --mpa-ink-brand/--mpa-line/--mpa-hairline) so a response reads as part
   of Solis, not a rendered markdown file. ============================ */
.ask-claude-messages h1, .mpa-md h1,
.ask-claude-messages h2, .mpa-md h2,
.ask-claude-messages h3, .mpa-md h3,
.ask-claude-messages h4, .mpa-md h4,
.ask-claude-messages h5, .mpa-md h5,
.ask-claude-messages h6, .mpa-md h6 {
  font-family: var(--mpa-font);
  color: var(--mpa-ink);
  letter-spacing: -.01em;
  line-height: 1.3;
  margin: .65rem 0 .3rem;
}
.ask-claude-messages h1:first-child, .mpa-md h1:first-child,
.ask-claude-messages h2:first-child, .mpa-md h2:first-child,
.ask-claude-messages h3:first-child, .mpa-md h3:first-child,
.ask-claude-messages h4:first-child, .mpa-md h4:first-child,
.ask-claude-messages h5:first-child, .mpa-md h5:first-child,
.ask-claude-messages h6:first-child, .mpa-md h6:first-child { margin-top: 0; }
/* h1/h2 stronger weight (700) reads as a heading at a glance; h3+ (600)
   stays legible without the page-title-grade heaviness Rory flagged. */
.ask-claude-messages h1, .mpa-md h1 { font-size: 1.3rem;  font-weight: 700; }
.ask-claude-messages h2, .mpa-md h2 { font-size: 1.2rem;  font-weight: 700; }
.ask-claude-messages h3, .mpa-md h3 { font-size: var(--mpa-text-lg);  font-weight: 600; }
.ask-claude-messages h4, .mpa-md h4,
.ask-claude-messages h5, .mpa-md h5,
.ask-claude-messages h6, .mpa-md h6 { font-size: 1rem; font-weight: 600; margin-top: .5rem; }

.ask-claude-messages p, .mpa-md p { margin-bottom: .5rem; line-height: 1.55; }
.ask-claude-messages p:last-child, .mpa-md p:last-child { margin-bottom: 0; }

.ask-claude-messages ul, .mpa-md ul,
.ask-claude-messages ol, .mpa-md ol { padding-left: 1.25rem; margin-bottom: .5rem; line-height: 1.55; }
.ask-claude-messages li, .mpa-md li { margin-bottom: .2rem; }
.ask-claude-messages li:last-child, .mpa-md li:last-child { margin-bottom: 0; }
.ask-claude-messages li > ul, .mpa-md li > ul,
.ask-claude-messages li > ol, .mpa-md li > ol { margin-top: .2rem; margin-bottom: .2rem; }

/* Bold/strong: weight only, never a font-size bump -- the browser default
   for <strong> is already weight-only, but marked.js/user content can
   nest a heading-sized element inside bold text, so this is pinned
   explicitly rather than left to inheritance. */
.ask-claude-messages strong, .mpa-md strong,
.ask-claude-messages b, .mpa-md b { font-weight: 700; font-size: inherit; }

.ask-claude-messages a, .mpa-md a {
  color: var(--mpa-ink-brand);
  text-decoration: underline;
  text-decoration-color: rgba(55, 56, 58, .35);
  text-underline-offset: 2px;
}
.ask-claude-messages a:hover, .mpa-md a:hover { color: var(--mpa-action-hover); }

.ask-claude-messages blockquote, .mpa-md blockquote {
  margin: .4rem 0;
  padding: .1rem .75rem;
  border-left: 3px solid var(--mpa-line);
  color: var(--mpa-muted);
  font-style: normal;
}
.ask-claude-messages blockquote p:last-child, .mpa-md blockquote p:last-child { margin-bottom: 0; }

.ask-claude-messages hr, .mpa-md hr {
  border: none;
  border-top: 1px solid var(--mpa-hairline);
  margin: .6rem 0;
}

.ask-claude-messages .tbl-wrap, .mpa-md .tbl-wrap { overflow-x: auto; margin: .4rem 0; }
.ask-claude-messages table, .mpa-md table { width: auto; max-width: none; border-collapse: collapse; font-size: var(--mpa-text-sm); margin: 0; }
.ask-claude-messages table th, .mpa-md table th,
.ask-claude-messages table td, .mpa-md table td { border: 1px solid #dee2e6; padding: .25rem .55rem; white-space: nowrap; }
.ask-claude-messages table th, .mpa-md table th { background: var(--mpa-hairline); font-weight: 600; }
/* Fenced code blocks (marked.js -> <pre><code>) scroll INSIDE their own
   box instead of being silently clipped by the panel's overflow:hidden
   with no way to read the rest of the line — same containment principle
   as .tbl-wrap above, never the outer document. */
.ask-claude-messages pre, .mpa-md pre {
  overflow-x: auto;
  max-width: 100%;
  margin: .4rem 0;
  padding: .5rem .65rem;
  background: var(--mpa-hairline);
  border-radius: .4rem;
}
.ask-claude-messages code, .mpa-md code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .82em;
  background: var(--mpa-hairline);
  padding: .1em .35em;
  border-radius: 4px;
  word-break: break-word;
}
/* More specific than the bare .code rule above, so it wins the cascade
   without !important: reset the inline-code pill styling inside a <pre>
   (the block itself already carries the background/padding). */
.ask-claude-messages pre code, .mpa-md pre code {
  background: none;
  padding: 0;
  border-radius: 0;
  font-size: var(--mpa-text-sm);
  white-space: pre;
  word-break: normal;
}

/* ============================================================
   BACK-COMPAT — legacy classes still referenced by pages that
   have not been migrated yet. Now token-driven.
   ============================================================ */
.stat-card { border: none; border-radius: var(--mpa-radius-sm); }
.stat-card-mpa { background: var(--mpa-action) !important; color: var(--mpa-on-action) !important; border: none; }

/* ============================================================
   AUDIT TRAIL LIST (invoice + staff detail) — the timestamp column is
   fixed-width so every action label ("Generated", "Approved"…) starts at
   the same x (Rory, 3 Aug 2026). Wide enough for "03 Aug 2026, 17:06
   UTC" in this font; a longer legacy string just pushes its own row's
   label right rather than truncating (standing wrap-not-truncate rule).
   ============================================================ */
.mpa-audit-list li > .text-muted:first-child {
  display: inline-block; min-width: 11rem;
}

/* ============================================================
   MPA SELECT — the standard dropdown skin (4 Aug 2026, disciplines
   rework item 8). THE PATTERN for every <select> going forward:

       <select class="form-select mpa-select">…

   Why it exists: on some mobile browsers the lightly-restyled native
   .form-select still renders as the OS default control — unstyled,
   off-centre, and free to overflow the viewport when an option is long.
   .mpa-select removes the native appearance entirely and supplies its
   own chevron + sizing, so the control renders identically everywhere
   and can never exceed its container. Retrofit of the app's existing
   selects is a follow-up pass (same convention as the kebab rollout:
   introduce the shared component first, migrate surfaces separately).
   ============================================================ */
.mpa-select {
  display: block;
  width: 100%;
  max-width: 100%;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-color: var(--mpa-surface, #fff);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='m3 6 5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .8rem center;
  background-size: 14px 14px;
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  color: var(--mpa-ink);
  padding: .45rem 2.4rem .45rem .75rem;
  min-height: 42px;
  line-height: 1.4;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}
.mpa-select:focus {
  border-color: var(--mpa-action);
  box-shadow: 0 0 0 3px rgba(var(--mpa-action-rgb), .18);
  outline: none;
}
.mpa-select:disabled { background-color: var(--mpa-bg); color: var(--mpa-muted); }
/* IE-era arrow suppression for completeness */
.mpa-select::-ms-expand { display: none; }

/* ============================================================
   MPA FIELD — the control-height reconciler (Timetable V2, 21 Aug 2026).

   THE PROMOTION the design system already asked for. .mpa-pl-control
   (Private Lessons, PL-1B, ruling J) found and fixed the root cause:
   .mpa-select carries min-height:42px but Bootstrap's .form-control /
   .form-control-sm / .btn-sm do NOT, and no shared utility existed to
   reconcile them — so ANY form mixing the two gets visibly mismatched
   rows. That fix was deliberately scoped to .mpa-pl-drawer because a
   feature branch must not restyle the whole app, and its own comment
   named this promotion as the obvious follow-up.

   This is that follow-up. .mpa-field is the same reconciler, opt-in and
   global. Rory's exact Schedule-form complaints all reduce to this one
   cause:
     - "start-time field height differs from neighbouring selects"
       (<input type=time class=form-control-sm> ~31px vs .mpa-select 42px)
     - "Day and Start Time not vertically aligned"  (same, in one row)
     - "Add to timetable button a different height from Teacher/Venue"
       (.btn-sm ~31px vs .mpa-select 42px)
   Fixed by opting those controls into .mpa-field, NOT by per-element
   pixel overrides.

   OPT-IN, never a blanket restyle: nothing that does not carry the class
   changes. .mpa-pl-control is kept as an alias of the same declarations
   so Private Lessons V1 is byte-identically unaffected.
   ============================================================ */
.mpa-field,
.mpa-pl-drawer .mpa-pl-control {
  min-height: 42px;
  border-radius: var(--mpa-radius-sm);
  font-size: var(--mpa-text-md);
}
/* Buttons additionally need their LABEL centred in that height —
   Bootstrap's .btn centres its own text only because its padding happens
   to fill the box; once min-height exceeds the padded content the label
   sits high. flex+center is the layout-level fix, not a magic top margin. */
button.mpa-field,
a.mpa-field,
.btn.mpa-field {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
}
/* A .mpa-field label sits directly above its control on the same
   baseline as its neighbours' labels, with the same reserved height, so
   an optional/required hint on ONE label cannot push that control down
   relative to the others in the row. */
.mpa-field-label {
  display: block;
  font-size: var(--mpa-text-xs);
  font-weight: 700;
  color: var(--mpa-muted);
  text-transform: uppercase;
  letter-spacing: .02em;
  margin-bottom: .2rem;
  min-height: 1.05rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 575.98px) {
  /* Matches .mpa-select's own mobile rule: 16px stops iOS auto-zoom on
     focus, 44px is the minimum comfortable touch target. */
  .mpa-field { font-size: 16px; min-height: 44px; }
}

/* Teacher / Venue / Add-to-timetable row (12 Aug 2026 acceptance round 4,
   item 1) — explicit 3-row x 3-col CSS Grid, replacing the old Bootstrap
   flex columns + min-height-placeholder hack. The venue warning is its
   OWN grid row (row 3) below the controls, so it structurally CANNOT
   affect row 1 (labels) or row 2 (controls) height — those two rows are
   sized only by their own content. The warning can go back to a plain
   x-show/x-cloak toggle: hidden means its grid row is simply 0-height,
   nothing above it moves either way. */
.mpa-tva-row {
  display: grid;
  grid-template-columns: 3fr 3fr 2fr;
  grid-template-areas:
    "tlabel  vlabel  ."
    "tselect vselect addbtn"
    ".       vwarn   .";
  column-gap: .5rem;
  row-gap: .35rem;
  align-items: end;
}
.mpa-tva-tlabel  { grid-area: tlabel; }
.mpa-tva-vlabel  { grid-area: vlabel; }
.mpa-tva-tselect { grid-area: tselect; }
.mpa-tva-vselect { grid-area: vselect; }
.mpa-tva-addbtn  { grid-area: addbtn; }
.mpa-tva-vwarn   { grid-area: vwarn; }
@media (max-width: 767.98px) {
  .mpa-tva-row {
    grid-template-columns: 1fr;
    grid-template-areas: "tlabel" "tselect" "vlabel" "vselect" "vwarn" "addbtn";
  }
}

/* Rory's SECOND corrective (30 Aug 2026, §9) — root cause of "Room
   consumes an entire extra row, with huge unused width beside Teacher/
   Venue": Room used to be its own .mb-2 row entirely BELOW .mpa-tva-row,
   growing this section's height for every room-enabled venue even though
   Teacher/Venue never needed their own full 3fr/3fr width. .mpa-tva-row-4
   is the SAME grid, widened to a 4th named area (rlabel/rselect/rwarn) so
   Room shares the row with Teacher/Venue/Add instead — applied only via
   :class="hasRoomChoices ? 'mpa-tva-row-4' : ''" (index.html), so a
   room-less venue (the vast majority) renders the ORIGINAL 3-column grid,
   byte-for-byte unchanged. */
.mpa-tva-row-4 {
  grid-template-columns: 2.3fr 2.3fr 2fr 1.8fr;
  grid-template-areas:
    "tlabel  vlabel  rlabel  ."
    "tselect vselect rselect addbtn"
    ".       vwarn   rwarn   .";
}
.mpa-tva-rlabel  { grid-area: rlabel; }
.mpa-tva-rselect { grid-area: rselect; }
.mpa-tva-rwarn   { grid-area: rwarn; }
@media (max-width: 767.98px) {
  .mpa-tva-row-4 {
    grid-template-columns: 1fr;
    grid-template-areas:
      "tlabel" "tselect" "vlabel" "vselect" "vwarn"
      "rlabel" "rselect" "rwarn" "addbtn";
  }
}

/* ============================================================
   SCHEDULE A CLASS / WORKSHOP FORM (Timetable V2, 21 Aug 2026)

   Replaces the old Bootstrap `.row.g-2.align-items-end` for row 1. That
   utility aligned columns on their BOTTOM edge, which is exactly why a
   31px time input sat visibly low beside a 42px select and why a
   two-line label ("Start time (24hr, e.g. 14:30)") pushed its own
   control down relative to its neighbours. Here every cell is a
   label-over-control stack in a grid row with `align-items: end`, and
   the labels themselves have a reserved min-height (.mpa-field-label),
   so labels share one baseline and controls share another — regardless
   of how long any individual label is.
   ============================================================ */
.mpa-sched-form { display: block; }
.mpa-sched-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: .5rem .6rem;
  align-items: end;
}
.mpa-sched-cell { min-width: 0; }
.mpa-sched-cell-wide { grid-column: span 2; }
@media (max-width: 767.98px) {
  .mpa-sched-row { grid-template-columns: minmax(0, 1fr); }
  .mpa-sched-cell-wide { grid-column: auto; }
}

/* Workshop date rows. Date gets the room, then time, then length, then a
   fixed-width remove button — the same "the important field is widest"
   rule the PL appointment card follows. Collapses to a two-up grid on
   phones rather than four squeezed columns. */
.mpa-ws-block {
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  background: var(--mpa-bg);
  padding: .6rem .65rem;
  margin-top: .75rem;
}
.mpa-ws-row {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) 3rem;
  gap: .4rem .5rem;
  align-items: end;
  margin-bottom: .4rem;
}
@media (max-width: 575.98px) {
  .mpa-ws-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  .mpa-ws-remove { grid-column: 2; justify-self: end; width: 3rem; }
}

/* The roster question. Tinted panel so it reads as a decision the admin
   is being asked to make, not another optional field. */
.mpa-roster-choice {
  border: 1px solid var(--mpa-line);
  border-left: 3px solid var(--mpa-action);
  border-radius: var(--mpa-radius-sm);
  background: var(--mpa-surface-2, #f8f9fa);
  padding: .6rem .7rem;
  margin-top: .75rem;
}

/* Availability panel. Zero-height when empty so nothing shifts while a
   probe is in flight. */
.mpa-avail:empty { display: none; }
.mpa-avail { margin-top: .25rem; }
/* Scheduling Availability / Conflicts UX V1 (26 Aug 2026) — suggested
   alternative-time chips, rendered as real <button>s so a click can carry
   the value straight into the time field (no separate JS state machine).
   Reuses .mpa-chip.brand for colour; only resets the browser's default
   button chrome and adds the click affordance a chip doesn't need on its
   own (the read-only chips elsewhere in the app are never clickable). */
.mpa-avail-suggest-btn {
  border: 0;
  cursor: pointer;
}
.mpa-avail-suggest-btn:hover,
.mpa-avail-suggest-btn:focus-visible {
  filter: brightness(0.94);
  outline: 2px solid var(--mpa-action);
  outline-offset: 1px;
}
.mpa-avail-row:last-child { margin-bottom: 0 !important; }
.mpa-pl-suggest {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  margin-left: .4rem;
  flex-wrap: wrap;
}

/* Roster action ghosts (12 Aug 2026) — quiet icon buttons with restrained
   semantic colour instead of flat grey or solid coloured circles: the
   -ink shade (already used for text on light chip backgrounds) is muted
   enough to sit next to a student's name without competing, and the
   matching -bg tint gives a subtle hover/focus wash. No background by
   default. Remove-from-class keeps its own btn-outline-danger untouched. */
.mpa-roster-action { border: 0; background: transparent; border-radius: var(--mpa-radius-sm); }
.mpa-roster-action.warn { color: var(--mpa-warn-ink); }
.mpa-roster-action.warn:hover, .mpa-roster-action.warn:focus-visible { background: var(--mpa-warn-bg); }
.mpa-roster-action.ok { color: var(--mpa-ok-ink); }
.mpa-roster-action.ok:hover, .mpa-roster-action.ok:focus-visible { background: var(--mpa-ok-bg); }
.mpa-roster-action.info { color: var(--mpa-info-ink); }
.mpa-roster-action.info:hover, .mpa-roster-action.info:focus-visible { background: var(--mpa-info-bg); }

/* Open-state option list (Rory, 6 Aug 2026 — investigated before fixing):
   the CLOSED box above is fully page-rendered (appearance:none hands it to
   our CSS), but the OPTION LIST that drops down on click is a native
   OS-drawn popup, not page content -- confirmed empirically: opening one
   in Chrome/Windows blocks the page's own screenshot/paint pipeline until
   it closes, the signature of a surface outside the page's compositor.
   That native surface only reads a few CSS properties (background-color,
   color, font-family on `option`) and, on Windows in particular, often
   can't reach a @font-face webfont at all -- it silently falls back to
   the OS UI font (Segoe UI) even though we ask for Plus Jakarta Sans,
   which is exactly why the open list reads differently from the closed
   box. This rule is the honest ceiling: it nudges colour/font wherever
   the browser's native popup honours it, but CANNOT round that popup's
   corners (no stable CSS hook exists for it) or guarantee the webfont
   renders -- those need a fully custom, page-rendered listbox (the
   .mpa-combo pattern already used for the student filter) to fix for
   real, which is a bigger component-swap than this pass, not a CSS fix. */
.mpa-select option {
  font-family: var(--mpa-font);
  background-color: var(--mpa-surface, #fff);
  color: var(--mpa-ink);
}
@media (max-width: 575.98px) {
  /* 16px stops iOS auto-zoom on focus; 44px is the minimum comfortable
     touch target. Full width inside whatever column wraps it. */
  .mpa-select { font-size: 16px; min-height: 44px; }
}

/* Stage 2.3.1 acceptance fix (14 Aug 2026, item 4): the occurrence-edit
   drawer's Teacher/Venue selects pair .mpa-select (its own unconditional
   padding/font-size — deliberately full-size for most of the app's
   pages) with Bootstrap's .form-select-sm, but .mpa-select's own rule
   wins on specificity, so they rendered ~44px/16px next to the Date/
   Time/Duration fields' .form-control-sm ~31px/14px in the SAME row
   (Rory, 14 Aug 2026 acceptance round). Scoped to this one form (not a
   bare .mpa-select override, which many other pages use deliberately at
   full size) and gated above the 575.98px mobile block above so that
   block's iOS-zoom/touch-target sizing still wins on phones — this is a
   desktop alignment fix only, not a mobile redesign. */
@media (min-width: 576px) {
  .mpa-occ-edit-form .mpa-select {
    padding: .25rem .5rem;
    padding-right: 2rem;
    font-size: var(--mpa-text-md);
    /* Rory's FOURTH corrective (30 Aug 2026, §H.1) — root cause of "Time/
       Duration still look taller than Day/Teacher/Venue/Room": this was a
       calc() APPROXIMATION (~31px) of .mpa-field-sm's own EXPLICIT
       `height: 33px`, close enough to read as "matching" in the 14 Aug
       fix that introduced it but never actually pixel-identical. Pinned
       to the literal same 33px .mpa-field-sm itself uses, with an
       explicit height (not just min-height) so it can't independently
       grow past it the way min-height alone could. */
    height: 33px;
    min-height: 33px;
    background-size: 12px 12px;
    background-position: right .65rem center;
  }
}

/* Stage 2.3.1 acceptance fix (14 Aug 2026, item 2): occurrence Save/
   Reset loading state. .occ-edit-busy is the htmx-indicator (opacity-
   toggled by htmx itself, per its documented hx-indicator contract —
   the SAME mechanism every other spinner in this app already relies on)
   but ALSO forced to display:none by default / display:inline-flex only
   while htmx has added .htmx-request to it — a plain opacity toggle
   would leave the spinner+"Saving…" text an invisible but SPACE-
   OCCUPYING gap ahead of the idle label at rest, which reads as a
   layout bug on a small button. The sibling selector then hides the
   idle label for exactly as long as the busy label is active — no JS,
   keyed off the one class htmx guarantees to toggle on the indicator
   element itself, so it can't drift out of sync with the real request
   lifecycle (covers success, 409 refusal, and network failure alike;
   hx-disabled-elt on the button — set alongside this in panel.html —
   uses the same guarantee for the disabled state). */
.occ-edit-busy {
  display: none;
  align-items: center;
  justify-content: center;
}
.occ-edit-busy.htmx-request {
  display: inline-flex;
}
.occ-edit-busy.htmx-request ~ .occ-edit-idle {
  display: none;
}

/* Stage 2.3.3 (15 Aug 2026, item 2): the occurrence-drawer refusal/
   success alerts (panel.html) — replaces Bootstrap's bare .py-2/.small
   combo (padding-Y only, leaving default 1rem padding-X, and no real
   vertical-alignment contract between an inline icon glyph and
   white-space:pre-line text) with a deliberate flex layout. Icon aligns
   to the FIRST LINE (align-items:flex-start + a small downward nudge —
   never center, which would drag a short icon to the vertical middle of
   a multi-line message and look worse, not better) and never shrinks;
   text wraps and stays inside the box even for one long unbroken token.
   Still alert-danger/alert-success for Bootstrap's own colour/border —
   only spacing/alignment changes; id/tabindex/role="alert" and the
   auto-scroll/focus script (item 4 of Stage 2.3.2) are untouched. */
.mpa-panel-alert {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  padding: .6rem .75rem;
  font-size: var(--mpa-text-sm);
  line-height: 1.45;
}
.mpa-panel-alert > .bi {
  flex-shrink: 0;
  line-height: 1.45;
  margin-top: .05rem;
}
.mpa-panel-alert-text {
  min-width: 0;
  overflow-wrap: break-word;
  white-space: pre-line;
}

/* x-show vs d-flex (4 Aug 2026, disciplines rework): Bootstrap's .d-flex
   is display:flex!important, which BEATS the inline display:none Alpine's
   x-show sets — an x-show'd element wearing d-flex can never hide. Any
   flex element toggled by x-show wears THIS class instead (same layout,
   no !important, so Alpine's inline style wins when hidden). */
.mpa-flex-row { display: flex; align-items: center; gap: .5rem; }

/* Stage 2.4a manual-acceptance fix (17 Aug 2026): a <button class="btn
   btn-link"> and a plain <a> sitting side by side (e.g. the timetable
   drawer's "Undo changes for this session" / "Cancel this session" row)
   must render pixel-identical. Matching font-size/line-height alone
   (the earlier 15 Aug fix) wasn't enough — Bootstrap's .btn still
   reserves a 1px transparent border and forces display:inline-block,
   neither of which a bare <a> ever has, so the button sat measurably
   taller/differently-baselined in a flex row. This strips exactly those
   two box-model properties down to plain inline text; colour/weight/
   underline/hover already come from .btn-link (kept alongside this
   class, not replaced by it) and are untouched. Apply to BOTH the
   button and its sibling <a> so they're governed by one shared rule. */
.mpa-inline-link-btn {
  display: inline;
  border: 0;
  vertical-align: baseline;
  font: inherit;
  line-height: inherit;
}

/* Stage 2.4b-2 (17 Aug 2026) — timetable drawer progressive disclosure.
   Native <details>/<summary> (§1 — no bespoke accordion framework);
   these classes are pure presentation, restrained on purpose (§6 — one
   coherent drawer, not a stack of boxes): a hairline divider between
   sections, a plain-text summary with the browser's own disclosure
   marker (recoloured to match the design system, never a second custom
   chevron icon on top of it), and no background wash on either state —
   the existing tinted "This class only" box and the series-edit
   preview/alert boxes inside a section still carry their own colour,
   unchanged; the SECTION shell itself stays neutral. */
.mpa-drawer-section {
  border-top: 1px solid var(--mpa-line);
  padding: .45rem 0;   /* density pass (9 Oct 2026): was .55rem — a collapsed desktop row 35.5 → 33px */
}
/* The first section (Students) is a heading straight after the status block (the break-even line).
   Round 2 (10 Oct 2026): ONE tight step between them, owned by the section — the summary adds no top
   padding of its own here. (Density pass had the GROUP step plus the summary's .2rem, plus each line's
   half-leading: ~9px of text gap that read as a stray blank line; 0 had "Students" touching it.) */
.mpa-drawer-section:first-of-type { border-top: none; padding-top: var(--mpa-space-tight); }
.mpa-drawer-summary {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .35rem;
  cursor: pointer;
  list-style: revert;
  padding: .2rem 0;
  font-size: var(--mpa-text-sm);
  font-weight: 700;
  color: var(--mpa-ink);
  -webkit-tap-highlight-color: transparent;
}
/* §5 mobile: the summary itself must wrap and never force horizontal
   scroll — a long "Usual schedule · Wednesday · 18:00 · Every other
   week" line on a narrow phone wraps onto a second line instead of
   pushing the marker/chip off-screen. */
.mpa-drawer-summary-text {
  min-width: 0;
  overflow-wrap: anywhere;
}
/* Round 2: the first section's summary adds no top padding of its own (see the first-of-type note above). */
.mpa-drawer-section:first-of-type > .mpa-drawer-summary { padding-top: 0; }
.mpa-drawer-summary::marker { color: var(--mpa-muted); font-size: .85em; }
.mpa-drawer-summary:focus-visible { outline: 2px solid var(--mpa-info); outline-offset: 2px; border-radius: 4px; }
/* Density pass (9 Oct 2026): a section heading → its first content is the TIGHT step (was .6rem,
   which with the summary's own padding and the first row's padding read as a blank line). The last
   block in a body drops its utility margin-bottom: the section's own padding is the only gap before
   the next hairline (the mb-2 + padding pair doubled it). */
.mpa-drawer-section-body { padding-top: var(--mpa-space-tight); }
.mpa-drawer-section-body > :last-child { margin-bottom: 0 !important; }
/* §5: a large-enough tap target on touch without inflating desktop
   density — 44px is the usual touch-target floor.
   TOUCH TARGET HEIGHT IS NOT VISUAL GAP (density pass, 9 Oct 2026 —
   supersedes corrective 3's body-padding tweak, which still left the
   label ~15px above the first pupil). The 44px summary IS the
   section's breathing room on touch, so:
   - the section adds no padding of its own above the summary, and a
     COLLAPSED section none below it either (a collapsed row was 63px:
     .55rem + 44px + .55rem; now 44px + the hairline);
   - an OPEN section's label sits at the BOTTOM of its 44px box: the
     spare height lands ABOVE the heading (separating it from the
     previous block, where space belongs) instead of between the heading
     and its own content. Hit area unchanged; no negative margins.
   Guarded by qa/test_layout_spacing_regression.py (§T / §D). */
@media (hover: none) and (pointer: coarse) {
  .mpa-drawer-summary { min-height: 44px; }
  .mpa-drawer-section, .mpa-drawer-section:first-of-type { padding-top: 0; }
  .mpa-drawer-section:not([open]) { padding-bottom: 0; }
  .mpa-drawer-section[open] > .mpa-drawer-summary { align-items: flex-end; padding-bottom: .1rem; }
  /* Round 2: the FIRST section (Students) follows the status block, where a 44px box put ~20px of air
     under the break-even line. Its tap target is still 44px — 30px of box plus an invisible hit area
     (::before) reaching 14px up over the status line, which holds nothing to tap — so the target no
     longer adds layout height. No negative margins; the box itself never overlaps anything. */
  .mpa-drawer-section:first-of-type > .mpa-drawer-summary { min-height: 30px; position: relative; }
  .mpa-drawer-section:first-of-type > .mpa-drawer-summary::before { content: ""; position: absolute; left: 0; right: 0; top: -14px; bottom: 0; }
}

/* Native <details>/<summary> collapsible CARDS (26 Aug 2026 acceptance
   corrective) — distinct from .mpa-drawer-section above (a compact
   disclosure NESTED inside a card body, e.g. Class Details' Price
   history). This variant makes the WHOLE CARD HEAD the toggle: a card
   wraps its body in <details data-collapse-key="..."><summary
   class="mpa-card-head">...title/chips...</summary><div
   class="mpa-card-body">...</div></details>. .mpa-card-head's own
   styling is completely unchanged; only the browser's default disclosure
   marker is suppressed. Used by Class Details (Parent-facing information,
   Term by term) and the Classes list (private-lesson discipline
   sub-groups — replacing a card nested inside another card, which was the
   root cause of a reported mobile misalignment: two independent scroll/
   frozen-column containers, one inside the other, desyncing at narrow
   widths). State is remembered per device via a small inline script on
   each page (localStorage, keyed by data-collapse-key) — no migration, no
   account setting for this nicety. #}
.mpa-card > details > summary { list-style: none; cursor: pointer; }
.mpa-card > details > summary::-webkit-details-marker { display: none; }
.mpa-collapse-chevron { transition: transform .15s ease; }
.mpa-card > details[open] > summary .mpa-collapse-chevron { transform: rotate(180deg); }

/* ── Two final tweaks (29 Aug 2026), §2 — a card-head control (Student
   Detail's Enrolments & Billing Term selector / Report Cards' "View all
   report cards") shares the desktop header band with its <summary>'s
   title instead of sitting on a wasted second row beneath it. The
   control is a normal DOM sibling AFTER </summary> -- NEVER nested
   inside the disclosure trigger, because a <select>/<a> inside <summary>
   would also toggle the parent <details> on click/Enter (the reason it
   was pulled out to begin with). CSS-only positioning over that row is
   the "established header-grid" alternative: real interaction semantics
   are untouched (this element structurally cannot trigger the summary's
   toggle), only its visual position moves.
   Desktop only (>=768px, this app's standing mobile breakpoint) — below
   that the control reverts to plain in-flow stacking beneath the header,
   the already-accepted mobile layout, completely unchanged. */
@media (min-width: 768px) {
  .mpa-details-header-control { position: relative; }
  /* top is a fixed offset, not top:0/bottom:0 spanning the whole
     <details> box (which would vertically center this against the
     ENTIRE open card's height, not just the header row) -- it lands the
     control in the middle of .mpa-card-head's own padding: 1rem .75rem;
     rem-based, so it holds if that padding rhythm ever changes in
     lockstep. right:2.5rem clears the chevron (small icon + summary's
     own 1.25rem right padding). */
  .mpa-details-header-control > .mpa-card-head-control {
    position: absolute;
    top: .5rem;
    right: 2.5rem;
    display: flex !important;
    align-items: center;
    padding: 0 !important;
    margin: 0 !important;
    width: auto;
  }
}

/* ============================================================
   PRIVATE LESSONS — drawer scheduling UI (PL-1B, 21 Aug 2026)
   ============================================================
   Scoped to .mpa-pl-drawer throughout: this branch must not restyle any
   existing page (§J's "no risky whole-application visual rewrite").

   CONTROL HEIGHT (ruling J) — root cause found: .mpa-select carries
   min-height:42px but Bootstrap's .form-control/.form-control-sm do not,
   and no shared utility existed to reconcile them, so ANY form mixing the
   two gets visibly mismatched rows. .mpa-pl-control is that reconciler,
   opt-in and scoped here.

   ✅ PROMOTED (Timetable V2, 21 Aug 2026): the declarations now live with
   .mpa-field in the MPA FIELD block above, which .mpa-pl-control shares
   verbatim as an alias — Private Lessons renders byte-identically, and
   the Schedule form finally has the same reconciler to opt into instead
   of the per-element pixel overrides Rory kept finding. */
.mpa-pl-drawer .form-label {
  font-size: var(--mpa-text-xs);
  font-weight: 700;
  color: var(--mpa-muted);
  text-transform: uppercase;
  letter-spacing: .02em;
  margin-bottom: .2rem;
}

/* Shared setup: two even columns that collapse to one on a narrow drawer
   (the drawer is 460px on desktop and full-screen on mobile). */
.mpa-pl-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .5rem .6rem;
}
.mpa-pl-field { min-width: 0; }
@media (max-width: 400px) {
  .mpa-pl-grid { grid-template-columns: minmax(0, 1fr); }
}

/* One appointment = one card, stacked vertically. Deliberately NOT a
   table: the PL-1 acceptance defect was a table-responsive wrapper whose
   overflow clipped the absolutely-positioned combo dropdown, and fixed
   column widths that squeezed pupil names into unreadability. */
.mpa-pl-appt {
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  padding: .55rem .6rem;
  margin-bottom: .5rem;
  background: var(--mpa-surface, #fff);
}
.mpa-pl-appt-head {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap;
}
.mpa-pl-time { flex: 0 0 7.5rem; min-width: 7.5rem; }
.mpa-pl-avail { flex: 1 1 auto; min-width: 0; }
.mpa-pl-avail .mpa-chip {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: left;
}
.mpa-pl-remove { flex: 0 0 auto; }

/* Pupils get the most room (ruling H) — full width, one per line, names
   wrap rather than truncate. */
.mpa-pl-pupils { margin-top: .45rem; display: flex; flex-direction: column; gap: .35rem; }
.mpa-pl-pupil { min-width: 0; }
.mpa-pl-pupil-search { width: 100%; min-width: 0; }
.mpa-pl-pupil-picked {
  display: flex;
  align-items: center;
  gap: .35rem;
  flex-wrap: wrap;
  padding: .35rem .5rem;
  border: 1px solid var(--mpa-hairline);
  border-radius: var(--mpa-radius-sm);
  background: var(--mpa-bg);
  min-height: 42px;
}
.mpa-pl-pupil-name {
  font-size: var(--mpa-text-md);
  font-weight: 600;
  min-width: 0;
  overflow-wrap: anywhere;
}
.mpa-pl-grade.is-unset { opacity: .65; font-style: italic; }
.mpa-pl-profile { color: var(--mpa-muted); text-decoration: none; font-size: var(--mpa-text-sm); }
.mpa-pl-profile:hover { color: var(--mpa-action); }
.mpa-pl-addpupil { align-self: flex-start; }

/* Combo dropdowns inside the drawer render IN FLOW, not absolutely —
   the same treatment .mpa-sheet already uses for the mobile filter sheet,
   and the reason the list can never be clipped by the drawer body's own
   scroll container. */
.mpa-pl-drawer .mpa-pl-combo-list {
  position: static;
  margin-top: 4px;
  max-height: 240px;
}
.mpa-pl-result {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .4rem;
  flex-wrap: wrap;
}
.mpa-pl-result-name { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.mpa-pl-result-meta { display: flex; gap: .25rem; flex-wrap: wrap; align-items: center; }
.mpa-pl-result.is-blocked { opacity: .55; cursor: not-allowed; }
.mpa-pl-result.is-blocked:hover { background: transparent; }
/* Timetable's own pupil picker (_student_search_results.html) reuses this
   same "disabled with a reason" house pattern on Bootstrap's plain
   .list-group-item-action, so it needs the identical dimmed/no-hover
   treatment rather than a third bespoke rule. */
.list-group-item-action.is-blocked { opacity: .55; cursor: not-allowed; }
.list-group-item-action.is-blocked:hover { background: transparent; }
/* Visual pass fix (26 Aug 2026): .mpa-chip's base white-space:nowrap is
   correct for short chips, but a clash-reason chip carries a real sentence
   ("Ballet Grade 2, Tue 18 Aug 18:00-18:45") that must be free to wrap onto
   its own line(s) at phone widths -- otherwise, even after flex-wrap drops
   it below the name, the chip's own nowrap text still runs past the row and
   the .list-group's overflow-y:auto ancestor silently grows overflow-x:auto
   underneath it (no visible scrollbar, no ellipsis -- just missing text).
   Same fix in both pupil pickers (timetable's plain .list-group and PL's
   .mpa-pl-result-meta), one house rule rather than two. */
.list-group-item-action .mpa-chip.danger,
.mpa-pl-result-meta .mpa-chip.danger {
  white-space: normal;
  text-align: left;
}

.mpa-pl-more { margin-top: .45rem; }
.mpa-pl-more-summary {
  font-size: var(--mpa-text-xs); color: var(--mpa-muted); cursor: pointer; padding: .15rem 0;
}
.mpa-pl-more-body { padding-top: .35rem; }

.mpa-pl-actions {
  position: sticky;
  bottom: 0;
  background: var(--mpa-surface, #fff);
  border-top: 1px solid var(--mpa-line);
  padding-top: .6rem;
  margin-top: .8rem;
}

/* Drawer roster name → pupil profile (PL-1B ruling K). Inherits the row's
   colour so a linked and an unlinked (no-permission) name look identical
   until hover — the roster must not read as two different kinds of row. */
.mpa-roster-link { color: inherit; text-decoration: none; }
.mpa-roster-link:hover,
.mpa-roster-link:focus-visible { color: var(--mpa-action); text-decoration: underline; }

/* ============================================================
   MPA FIELD — the shared control-height reconciler (21 Aug 2026)
   ============================================================
   THE PROMOTION PL-1B ASKED FOR. Private Lessons V1 found and recorded
   the root cause of a complaint Rory has now raised on four separate
   pages: ".mpa-select carries min-height:42px but Bootstrap's
   .form-control/.form-control-sm do not, and no shared utility existed
   to reconcile them, so ANY form mixing the two gets visibly mismatched
   rows." It built .mpa-pl-control as a drawer-scoped reconciler and
   noted "promoting it to a global .mpa-field is the obvious follow-up,
   deliberately NOT done in a feature branch."

   This is that promotion — with the same caution that kept PL-1B scoped:
   IT IS OPT-IN. Nothing changes anywhere until a template adds the
   class, so a page that has already been through acceptance cannot
   regress. (Verified before landing: `mpa-field` appeared nowhere in
   app/ except PL-1B's own comment.)

   TWO TIERS, both taken from what the app ALREADY ships — no invented
   pixel values:
     .mpa-field      42px — the standard control height, the number
                     .mpa-select and .mpa-pl-control already use.
     .mpa-field-sm   33px — the compact height .mpa-filter-bar already
                     pins its .form-control-sm/.form-select-sm/.btn-sm to
                     so a filter row lines up. Use inside dense tables.

   Applies to inputs, selects, .mpa-select AND buttons, because a row is
   only aligned if the button matches the field beside it. Buttons get
   inline-flex centring so their label sits on the control's optical
   centre instead of riding high (a specific Rory complaint).

   Mobile: 44px + 16px font on the standard tier — 16px is what stops iOS
   auto-zooming a focused input, and 44px is the minimum comfortable
   touch target. Identical reasoning (and numbers) to .mpa-select's own
   mobile block above.
   ============================================================ */
.mpa-field,
input.mpa-field,
select.mpa-field,
textarea.mpa-field {
  min-height: 42px;
  border-radius: var(--mpa-radius-sm);
}
/* Rory's SECOND Rooms V1 acceptance corrective (30 Aug 2026) — root
   cause of "Cover/Venue = white, Rate/Cancel reason = grey": .mpa-field
   has only ever reconciled HEIGHT (this rule's own original scope), never
   background — a plain <input class="mpa-field"> still inherited
   Bootstrap's own .form-control background, which is var(--bs-body-bg)
   (== --mpa-bg, the pale app-canvas colour, NOT white) in this app's
   Bootstrap variable overrides. .mpa-select sits right beside it with an
   EXPLICIT var(--mpa-surface, #fff) background, so the two never matched.
   Scoped to plain input/textarea only (never select or button, which
   already carry their own correct backgrounds via .mpa-select or their
   own btn-* colour) to keep this an additive, low-risk fix consistent
   with .mpa-field's own deliberately opt-in history just above. */
input.mpa-field,
textarea.mpa-field {
  background-color: var(--mpa-surface, #fff);
}
input.mpa-field:disabled,
input.mpa-field:read-only,
textarea.mpa-field:disabled,
textarea.mpa-field:read-only {
  background-color: var(--mpa-bg);
}
.btn.mpa-field {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  line-height: 1.2;
  white-space: nowrap;
}
.mpa-field-sm,
input.mpa-field-sm,
select.mpa-field-sm,
textarea.mpa-field-sm {
  min-height: 33px;
  height: 33px;
  border-radius: var(--mpa-radius-sm);
}
textarea.mpa-field-sm { height: auto; }
.btn.mpa-field-sm {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .3rem;
  line-height: 1;
  white-space: nowrap;
  padding-top: 0;
  padding-bottom: 0;
}
/* .mpa-select's own padding/min-height would otherwise beat the compact
   tier (it is a single class, same specificity, but declared earlier —
   these come last in the file so they win on document order). */
select.mpa-select.mpa-field-sm {
  padding-top: .15rem;
  padding-bottom: .15rem;
  font-size: var(--mpa-text-md);
  background-size: 12px 12px;
  background-position: right .6rem center;
  padding-right: 1.9rem;
}
/* ROW ALIGNMENT — two variants, because one rule genuinely cannot serve
   both shapes (found by measuring the rendered page, not by reading it):
     .mpa-field-row      bottom-aligns. For a picker row where one column
                         is a bare button with NO label, so the button
                         must drop to sit level with the controls.
     .mpa-field-row-top  top-aligns. For a row where every column HAS a
                         label but only some carry helper text underneath
                         — bottom-alignment there pushes the short column
                         DOWN by the height of the other one's helper
                         text, which is exactly how "Total seats
                         available" and "Venue hire cost" ended up on
                         different lines. */
.mpa-field-row { align-items: flex-end !important; }
.mpa-field-row-top { align-items: flex-start !important; }

@media (max-width: 575.98px) {
  .mpa-field,
  input.mpa-field,
  select.mpa-field,
  textarea.mpa-field { min-height: 44px; font-size: 16px; }
  .btn.mpa-field { min-height: 44px; }
  /* Dense-table controls still must not trigger iOS zoom on focus. */
  .mpa-field-sm,
  input.mpa-field-sm,
  select.mpa-field-sm { min-height: 40px; height: 40px; font-size: 16px; }
  .btn.mpa-field-sm { min-height: 40px; height: 40px; }
  select.mpa-select.mpa-field-sm { font-size: 16px; }
}

/* ============================================================
   DRAWER "ADD PUPIL" SEARCH (Rory, 22 Aug 2026)

   The picker was a bare .form-control-sm on the drawer's own tinted
   surface, so it read as background rather than as an input — Rory
   couldn't see where to type. Given a real input surface, the
   design-system border, a leading search icon and an accessible
   placeholder, while keeping .mpa-field's control height so it still
   lines up with every other drawer control.

   Deliberately SCOPED to this one control rather than restyling
   .form-control globally: a global change would touch every form in the
   app, which is not a safe thing to do inside a feature branch.
   ============================================================ */
.mpa-pupil-search-wrap { position: relative; }
.mpa-pupil-search-icon {
  position: absolute;
  left: .6rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--mpa-muted);
  font-size: var(--mpa-text-md);
  pointer-events: none;
}
.mpa-pupil-search {
  padding-left: 2rem;
  min-height: 42px;
  background-color: var(--mpa-surface, #fff);
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  color: var(--mpa-ink);
  font-size: var(--mpa-text-md);
}
/* WCAG: the default Bootstrap placeholder grey is too light on this
   surface. --mpa-muted is the token already used for secondary text. */
.mpa-pupil-search::placeholder { color: var(--mpa-muted); opacity: 1; }
.mpa-pupil-search:focus {
  border-color: var(--mpa-action);
  box-shadow: 0 0 0 3px rgba(var(--mpa-action-rgb), .18);
  outline: none;
}
@media (max-width: 575.98px) {
  /* Matches .mpa-select/.mpa-field: 16px stops iOS auto-zoom on focus,
     44px is the minimum comfortable touch target. */
  .mpa-pupil-search { font-size: 16px; min-height: 44px; }
}

/* Trial & Enrolment Lifecycle V1 manual-acceptance corrective (27 Aug
   2026): the pupil-search results list used to render IN-FLOW below the
   input, pushing the new Admission/Start-from/Trial-length controls
   further down the drawer's own scroll area — this search box sits at
   the BOTTOM of the roster section, so a downward-opening list had
   nowhere honest to grow without shoving those controls out of view.
   Floats upward instead, over the (already-scrolled, less load-bearing)
   roster names above — same floating-dropdown treatment .mpa-combo-list
   already established elsewhere in this app (surface/border/shadow/
   z-index reused verbatim, never a second dropdown convention), just
   anchored to the search wrap's own bottom edge instead of its top.
   .mpa-pupil-search-wrap is already position:relative (existing rule,
   above) — the anchor this floats against. */
.mpa-pupil-search-results:empty { display: none; }
.mpa-pupil-search-results {
  position: absolute; bottom: calc(100% + 4px); left: 0; right: 0; z-index: 1080;
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: 10px; box-shadow: var(--mpa-shadow-lift);
  /* Rory acceptance corrective (10 Sep 2026, item N) — root cause of
     "the first result is clipped and can't be selected": this box had
     no height cap at all (unlike its sibling .mpa-combo-list, which has
     always had max-height + overflow-y:auto — the exact rule this one
     was missing). With the drawer's full not-yet-enrolled student list
     rendered as one button per row, an uncapped box anchored to grow
     UPWARD from the input routinely extended past the top of the
     viewport/drawer, taking its topmost (first) results off-screen and
     out of click range. Capped and scrollable now, same convention as
     every other floating list in this app. */
  max-height: min(320px, 60vh); overflow-y: auto;
}
/* Item N: prefer opening BELOW the input when there is genuinely more
   room there than above it (JS-toggled, timetable/index.html's
   mpaPositionSearchResults — measures against the real viewport, so it
   naturally respects the drawer's own edges). This field usually sits
   near the bottom of the drawer's scroll area, where upward (the
   default above) is usually still the right call — this only flips it
   when the input is high enough up the screen that below is clearly
   better, never leaving the box uncapped either way. */
.mpa-pupil-search-results.opens-down {
  bottom: auto; top: calc(100% + 4px);
}

/* ============================================================
   CLASS PLANS V2A — FINAL RESPONSIVE CORRECTIVE (23 Aug 2026)
   Visual/responsive only. No target-grade semantics, curriculum
   retrieval, approval logic or generation logic sit anywhere near
   this block. Desktop is untouched throughout (every rule below
   is inside a max-width media query).
   ============================================================ */

/* §2 — Approved/draft/pushed plan header actions. The shared
   .mpa-head-actions mobile rule (line ~684, built for Students/Staff's
   2-button pairs) sets `.btn { flex: 1 1 0 }`, which removes each
   button's natural minimum width. With 2-3 buttons of uneven label
   length competing for one narrow row, the browser shrank a button
   below its own label's width and wrapped the TEXT inside it —
   confirmed in the browser: computed white-space was "normal" and
   min-width "0px" on the button itself. mpa-plan-actions (3-class
   selector, always wins on specificity regardless of source order)
   restores each button to its natural content width and lets WHOLE
   buttons wrap to a new row instead — a label is never split. */
@media (max-width: 767.98px) {
  .mpa-plan-actions.mpa-head-actions .btn {
    flex: 1 1 auto;
    white-space: nowrap;
  }
}

/* §7 — Breadcrumb. A private lesson's class name ("Private Singing —
   Quilliam Cardproof") is the second crumb and runs long. Plain
   Bootstrap reflowed it mid-text, stranding the "/" separator alone
   at the end of a line. Deliberate two-line structure instead: each
   crumb becomes its own line and the separator is hidden — there is
   nothing left for it to visually join once the items stack, so a
   visible "/" would itself become the stray mark this is fixing. */
@media (max-width: 767.98px) {
  .mpa-plan-breadcrumb.breadcrumb {
    flex-direction: column;
    align-items: flex-start;
    row-gap: .15rem;
  }
  .mpa-plan-breadcrumb .breadcrumb-item + .breadcrumb-item::before {
    display: none;
  }
  .mpa-plan-breadcrumb .breadcrumb-item.active { white-space: normal; }
}

/* §3 — Class overview card action. .mpa-card-head (shared by every
   .mpa-card in the app) has no flex-wrap, so its icon+title, status
   chip and the ms-auto "+ Add class overview" button all try to fit
   one row. On a narrow phone the button ran past the card's own right
   edge — .mpa-card's `overflow: hidden` (its border-radius clip) then
   silently cropped it, exactly as reported. Scoped to THIS card by id
   rather than changing .mpa-card-head for every card in the app: the
   action wraps onto its own full-width line instead of overflowing. */
@media (max-width: 767.98px) {
  #cp-overview-card .mpa-card-head {
    flex-wrap: wrap;
    row-gap: .5rem;
  }
  #cp-overview-card .mpa-card-head .mpa-link {
    flex: 1 1 100%;
    margin-left: 0;
    text-align: left;
  }
}

/* §4 — Per-pupil target rows (Create Plan, private lessons). The
   divider used to be Bootstrap's un-themed default .border-top on
   EVERY row including the first, landing right under the "Pupil /
   Current level / Target for this term" heading (pb-1's quarter-rem
   gap) — Bootstrap's default border colour is a plain #dee2e6, well
   off the app's own much softer --mpa-line/--mpa-hairline tokens used
   everywhere else, so it read as a stray line rather than a component
   boundary. The heading now owns one clean --mpa-line rule with real
   breathing room; the `+` sibling selector below means only rows
   AFTER the first ever get a divider, in the softer --mpa-hairline. */
.mpa-target-head-row {
  border-bottom: 1px solid var(--mpa-line);
  padding-bottom: .5rem;
  margin-bottom: .35rem;
}
.mpa-target-row + .mpa-target-row {
  border-top: 1px solid var(--mpa-hairline);
}
/* Mobile: each pupil becomes a labelled stacked block instead of three
   bare columns (the head row above is already d-none below sm) — a
   light card so 2:1/3:1 pupils stay visually separate, matching the
   look of every other card on this page rather than inventing a new
   one. mpa-target-label mirrors the "Current: " prefix pattern that
   already existed for the current-level column, extended to all three
   fields for symmetry. */
@media (max-width: 767.98px) {
  .mpa-target-row {
    background: var(--mpa-hairline);
    border-radius: var(--mpa-radius-sm, .5rem);
    padding: .75rem .85rem !important;
    margin-top: .5rem;
  }
  .mpa-target-row + .mpa-target-row { border-top: 0; }
  .mpa-target-row > div + div { margin-top: .5rem; }
  .mpa-target-label {
    display: block;
    font-size: var(--mpa-text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .02em;
    margin-bottom: .15rem;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   COMMUNICATIONS V1A — FINAL MANUAL-ACCEPTANCE CORRECTIVE (28 Aug 2026)
   §H Student Details on a phone · §I Enrolments & billing table width
   ══════════════════════════════════════════════════════════════════════ */

/* ── §H: long values must never leave the card ──────────────────────────
   The stacking half of this fix is in the template (.col-5/.col-7 ->
   .col-sm-5/.col-sm-7 — see students/detail.html's own note). This is the
   other half: a parent email, a portal address or a QBO customer id has
   no spaces to wrap at, so once the value column got narrow the text ran
   straight past the card edge (measured at 390px and 360px before the
   fix). overflow-wrap:anywhere breaks INSIDE the token only when there is
   no other option, so normal prose still wraps at spaces and nothing is
   hyphenated that didn't need to be.

   Scoped to .mpa-detail-dl, not to every <dl class="row"> in the app:
   this is the definition list the corrective was actually asked to fix,
   and a blanket rule would change wrapping on surfaces nobody reviewed.
   Applies at ALL widths — a long address can overflow a narrow desktop
   column too — and changes nothing that already fitted. */
.mpa-detail-dl dd,
.mpa-detail-dl dd a {
  overflow-wrap: anywhere;
  word-break: break-word;
}
/* The value column's own content (chips, buttons, the opted-in
   explanation) must not force the row wider than the card either. */
.mpa-detail-dl dd > * { max-width: 100%; }

@media (max-width: 575.98px) {
  /* Stacked mode: the label reads as a heading for the value beneath it,
     so it needs a little air above and none of the desktop gutter. */
  .mpa-detail-dl > dt { margin-bottom: .1rem; }
  .mpa-detail-dl > dd { margin-bottom: .65rem; }
  .mpa-detail-dl > dd:last-child { margin-bottom: 0; }
}

/* Staff Detail card geometry (PWA Responsive UI Consistency Polish V1,
   §10A): the card is .h-100 so it stretches to match Rate Summary's
   height when they sit side by side, but .mpa-card itself is not a flex
   container, so ALL of that stretch previously piled up as blank space
   below the footer buttons instead of being used. Making the card a flex
   column lets the footer (border-top divider + Resend invite/Disable
   login) grow to fill exactly that leftover space and vertically centre
   its buttons inside it, so the divider sits right after the last detail
   row and the buttons sit centred between it and the card's bottom edge
   -- no fixed pixel heights, this is the existing h-100/row stretch doing
   the equal-height alignment, just finally landing somewhere visible.
   On mobile the cards stack (one per row), so h-100 has no sibling to
   match and resolves to natural content height -- flex:1 then has
   nothing extra to grow into, so this is a no-op there by construction,
   not a separate breakpoint override. */
.mpa-staff-detail-card { display: flex; flex-direction: column; }
/* Two classes on purpose (19 Oct 2026): with one, the later desktop
   density rule ".mpa-card-body { padding: .3rem 1rem .85rem }" (>= 992px)
   won on source order and silently made this band .3rem over / .85rem
   under -- the button sat ~5px below the divider and ~12px above the card
   edge. The equal padding is the intent at every width. */
.mpa-card-body.mpa-staff-detail-footer {
  flex: 1 1 auto;
  align-items: center;
  align-content: center;
  padding-top: 1.1rem;
  padding-bottom: 1.1rem;
}
/* The last detail value carries no trailing margin: Bootstrap's dd
   margin-bottom (.5rem) otherwise stacks on the card body's own bottom
   padding, pushing the footer divider well below "Added". The <576px
   stacked rule above already does this for phones. */
.mpa-staff-detail-card .mpa-detail-dl > dd:last-child { margin-bottom: 0; }
/* Phones: the shared dl.mpa-dl grid (R5, below) pads every label/value .6rem
   top and bottom and drops the FIRST row's top padding; the last row's
   bottom padding then stacked on the card body's own, leaving ~29px of dead
   space above the footer divider. Mirror the first-row rule for the last
   row, on this card only. */
@media (max-width: 767.98px) {
  .mpa-staff-detail-card dl.row.mpa-dl > :is(dt, dd):nth-last-child(-n+2) { padding-bottom: 0; }
}

/* ── §I: Enrolments & billing at phone width ────────────────────────────
   ROOT CAUSE, measured at 390px before the fix: the shared mobile rule
   above (".table-responsive:has(.mpa-table) > .mpa-table.table") sets a
   min-width floor of 40rem/640px on EVERY .mpa-table. That floor is
   correct for Students/Staff/Invoices (many columns, colliding headers)
   but wrong here: this table has five columns, three of them short
   numbers/chips, and the only genuinely long content is the class name —
   which was being kept on one line and dragging the whole table out.

   .mpa-table-fluid is an OPT-IN modifier, exactly like the existing
   .mpa-table-narrow/.mpa-freeze-cap ones, so no other table's measured
   behaviour changes:
     - drops the floor from 40rem to 26rem, so the table sizes to its
       content and only scrolls when it genuinely must;
     - lets the first two columns (Term, Class) wrap across lines rather
       than dictating the table's width;
     - keeps ALL five columns and all data — the wrapper still scrolls if
       the remaining columns need it, so nothing is truncated and the page
       body itself never scrolls sideways. */
@media (max-width: 767.98px) {
  .table-responsive:has(.mpa-table.mpa-table-fluid) > .mpa-table.table {
    min-width: 26rem;
  }
  /* overflow-wrap: BREAK-WORD, deliberately not `anywhere`. Caught in
     review at 360px: `anywhere` also shrinks an element's MIN-CONTENT
     width to a single character, and under table-layout:auto the browser
     then happily gave the Class column ~1 character and stacked "Ballet
     Grade 2 (seed)" vertically, one letter per line. break-word wraps at
     word boundaries first and only breaks inside a word that genuinely
     cannot fit, and — the load-bearing difference — leaves min-content at
     the longest WORD, so the column can never collapse below a readable
     width. */
  .mpa-table.mpa-table-fluid thead th:nth-child(-n+2),
  .mpa-table.mpa-table-fluid tbody td:nth-child(-n+2) {
    white-space: normal;
    overflow-wrap: break-word;
  }
  /* THE actual thing that was holding the table open, measured rather
     than guessed: the class name is an <a class="mpa-link">, and
     .mpa-link carries `white-space: nowrap` globally (it exists for the
     "View all →" affordance in a card head, where wrapping WOULD be
     wrong). Inside this table's Class cell that rule forced "Modern
     Dance Inters (seed)" onto one line and dragged the whole table wider
     — exactly the brief's "scrolling an absurd distance merely because a
     class name was forced to stay on one line". Overridden only here:
     phone widths, this table, its first two columns. The link keeps its
     colour, weight and behaviour. */
  .mpa-table.mpa-table-fluid tbody td:nth-child(-n+2) .mpa-link,
  .mpa-table.mpa-table-fluid tbody td:nth-child(-n+2) a {
    white-space: normal;
    overflow-wrap: break-word;
  }
  /* No max-width cap on the first column: tried 30vw and measured it at
     360px, where capping Term squeezed the whole left side and made
     things worse rather than better. The 26rem floor plus wrapping is
     enough on its own here — this table's first cell is a short term
     name, not the long class name .mpa-freeze-cap exists for. */
}

/* ── Mobile Launch Polish V1 (28 Aug 2026) — §I's own 26rem floor was
   measured against the WRAPPED width of columns 1-2 only; columns 3-5
   ("Sessions attended" / "Invoice status" / "Tuition billed") keep their
   header text on one line (§I's wrap rule only covers nth-child(-n+2)),
   and at 390px those three un-wrapped header PHRASES — not their actual
   short data (a chip, a session count, a price) — were the real width
   floor: measured 150/123/117px per header vs the data itself needing
   well under half that. Letting every header wrap the same way §I already
   wraps Term/Class shrinks the table from a measured 575px to 419px (a
   session at 390px) — data cells were never the problem, so they are
   untouched. The 26rem (416px) floor is dropped to 0 for the same reason
   §I dropped 40rem to 26rem: it now sits ABOVE the table's genuine
   content width and would only force back the scroll this fix removes. */
@media (max-width: 767.98px) {
  .table-responsive:has(.mpa-table.mpa-table-fluid) > .mpa-table.table {
    min-width: 0;
  }
  .mpa-table.mpa-table-fluid thead th {
    white-space: normal;
    overflow-wrap: break-word;
  }
}

/* ── C1. Class Details — Class interest row collision (28 Aug 2026):
   parent name / "for [pupil]" / status chip / date were 4 flex children
   on one unwrapped d-flex line — fine on desktop's width, but at phone
   width the browser could only shrink each item's box (not reflow the
   text), so long values visually ran together. Template now groups them
   into two sub-rows (.mpa-interest-who, .mpa-interest-meta); this rule
   only changes the OUTER row to wrap at phone width, dropping "meta"
   (chip + date) to its own line under "who" (name + pupil) — both groups
   already wrap internally via their own flex-wrap. Desktop (both groups
   side by side on one line) is completely unchanged since the media
   query never applies there. */
@media (max-width: 767.98px) {
  .mpa-interest-row {
    flex-wrap: wrap;
    row-gap: .3rem;
  }
  .mpa-interest-row .mpa-interest-meta {
    flex-basis: 100%;
  }
}

/* ── D1. Attendance list — compact British date at phone width (28 Aug
   2026): "Wed 24 Jun 2026" was the single widest cell in the table,
   forcing extra horizontal scroll before reaching Roll Call. CSS-only
   swap between two server-rendered spans (both dates are already correct
   — no JS date formatting) rather than reformatting server-side, since
   desktop keeps the full weekday+year format unchanged. */
.mpa-att-date-compact { display: none; }
@media (max-width: 767.98px) {
  .mpa-att-date-full { display: none; }
  .mpa-att-date-compact { display: inline; }
}

/* ── D2. Attendance list — whole-row click to Roll Call (28 Aug 2026):
   cursor affordance only; the actual navigation is row_nav.js (loaded
   globally in base.html), reading each row's own data-row-href. */
.mpa-row-clickable { cursor: pointer; }

/* ── §6 (29 Aug 2026, FINAL FINAL acceptance corrective) — Staff list
   mobile row: ADAPTIVE one/two-line layout, replacing the previous
   .mpa-row-stack-sm (which forced EVERY row onto two lines regardless of
   whether the name+badges actually fit on one). Root cause of why a
   plain flex-wrap alone doesn't already do this: .mpa-row-main's shared,
   app-wide `flex: 1` shorthand is flex-basis:0% — for the wrap-fit
   algorithm that makes the name's HYPOTHETICAL size always ~0 regardless
   of its real content width, so the browser never considers wrapping the
   badge group away; it would just let the name's flex-grow silently
   squeeze the badges instead. Scoped to .mpa-row-adaptive so no other
   .mpa-row usage app-wide is affected: flex-basis:auto here makes the
   name's hypothetical size its REAL (max-content) width, so the wrap-fit
   test correctly evicts the badge group to its own line only once
   name+badges genuinely don't both fit — exactly "same line when they
   fit, wrap as a unit when they don't". flex-shrink:0 on the badge group
   stops it from being squeezed instead of wrapping. */
.mpa-row.mpa-row-adaptive { flex-wrap: wrap; }
.mpa-row.mpa-row-adaptive .mpa-row-main { flex: 1 1 auto; min-width: 8rem; }
.mpa-row.mpa-row-adaptive .mpa-row-side { flex: 0 0 auto; }

/* ── D3. Attendance list — teacher name / cover badge collision (28 Aug
   2026): the badge is deliberately position:absolute on desktop (the
   "non-shifting badge" pattern also used for the Today/override badges
   elsewhere in this table, all documented at their own call sites) so a
   cover/handover flag never shifts the teacher name's centred position —
   correct on desktop where the column has fixed room to spare. At phone
   width the table narrows enough that a real name plus that floating
   badge could overlap the next column (position:absolute contributes
   nothing to the cell's own width calculation, so the cell is sized to
   the name text alone and the badge floats over whatever sits beside it).
   Scoped to ONLY the teacher cell's own badges (.mpa-att-teacher-badge),
   not the Date cell's badge elsewhere in this same table, which has no
   width cap and still has fixed room at every width this app supports.
   (The Venue cell's own "override" badge had a DIFFERENT, desktop-width
   defect — a fixed 150px column clipping the floating badge outright,
   not a mobile overlap — fixed separately below, not here.) */
@media (max-width: 767.98px) {
  .mpa-att-teacher-name {
    white-space: normal;
    overflow-wrap: break-word;
    display: inline-block;
  }
  .mpa-att-teacher-badge {
    position: static !important;
    transform: none !important;
    display: inline-block;
    margin: .25rem 0 0 !important;
  }
}

/* ── Attendance list — Venue column "override" badge clipping (Rory UX
   follow-up, 10 Sep 2026): unlike every other badge in this table, the
   Venue cell also carries a fixed 150px column width (table-layout:
   fixed) with overflow:hidden — a long venue/room name pushed the old
   position:absolute "non-shifting" badge past that clip boundary, so it
   was cut off or invisible entirely, at DESKTOP width too (d-lg-table-
   cell — this column never renders below lg, so no media query is
   needed; the same flex layout applies at every width the column is
   shown, including a narrower laptop). No fixed width here at all —
   the badge is a normal flex sibling that is NEVER shrunk or clipped;
   the name gets a dedicated truncating span that gives way first
   (ellipsis) whenever the two don't both fit in the 150px box. */
.mpa-att-venue-cell { max-width: 150px; overflow: hidden; }
.mpa-att-venue-wrap { max-width: 100%; }
.mpa-att-venue-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mpa-att-venue-badge { flex-shrink: 0; margin-left: .25rem; }

/* ── §11. Attendance list — sticky class context (29 Aug 2026): the
   table's own header row deliberately gives up sticky below 768px (§9.7 —
   sticky and horizontal scroll can't coexist, scroll wins so no column is
   ever unreachable). That leaves the class name in the card head as the
   only place it lives; sticking THAT restores "which class am I looking
   at" while scrolling a long dated table, without a second banner element
   or repeating the title anywhere else. z-index above the table's own
   (frozen-column / dropdown) layers so it never gets scrolled under. */
@media (max-width: 767.98px) {
  .mpa-att-class-head {
    position: sticky;
    top: 0;
    z-index: 6;
    background: var(--mpa-surface);
  }
}

/* ── §12. Attendance list — action column width (29 Aug 2026): under
   table-layout:auto (mobile, §9.7) the action column sized itself to
   whatever was narrowest at that moment, letting "View / Edit" (the
   widest genuinely-used label) wrap onto 3 cramped lines. A mobile-only
   min-width gives it enough box to wrap at 2 lines maximum, matching
   Roll Call's existing wrap. */
@media (max-width: 767.98px) {
  .mpa-att-action-btn {
    min-width: 5.4rem;
    white-space: normal;
  }
}

/* ── §10 (29 Aug 2026, Rory FINAL acceptance corrective) — Invoice
   DESKTOP summary recomposition, superseded by §2 of the FINAL FINAL
   acceptance corrective (29 Aug 2026): the status tiles first moved from
   a right-floated col-xl-4 to a flush full-width 4-across row, but at
   desktop that just traded "floating in empty space" for "stretched
   across the entire card width" (still not what Rory asked for — a
   COMPACT status component). The card is now a genuine two-region split
   (financial ~60-65% left, status ~35-40% right — see invoices/index.html)
   at every width, so .mpa-inv-status-grid is simply a 2x2 grid
   UNCONDITIONALLY: there is no width at which 4-across-in-a-narrow-column
   was ever the right answer. Opt-in modifier, so .mpa-status-list
   elsewhere (Dashboard money card, Reports) is untouched. */
/* Final Forensic Corrective Pass (4 Sep 2026, §8): SIX tiles now
   (partials/_invoice_status_grid.html) -- 2 columns x 3 rows on phones,
   3 columns x 2 rows from 768px. Equal cells (1fr each, centred count +
   label), one geometry shared by Invoices, Dashboard and Reports. */
.mpa-status-list.mpa-inv-status-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: .9rem;
  column-gap: 1rem;
}
@media (min-width: 768px) {
  .mpa-status-list.mpa-inv-status-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 1.25rem; }
}
.mpa-inv-status-grid > div, .mpa-inv-status-grid > button, .mpa-inv-status-grid > a { text-align: center; min-width: 0; }
.mpa-inv-status-grid .mpa-status-name { justify-content: center; white-space: nowrap; }

/* ── Two final tweaks (29 Aug 2026), §1 — status summary tiles double as
   filter shortcuts: .mpa-status-tile strips <button> chrome back to the
   PLAIN look the (formerly bare <div>) tile already had, then layers on
   a subtle interactive/active treatment using existing tokens only — no
   new button style, this must still read as "the same metric", now
   clickable. text-decoration:none is for the Dashboard Invoice Status
   Hotfix V1 reuse of this class on <a> tiles (Invoice list's own tiles
   are <button>s, unaffected either way) — without it the browser's
   default link underline leaked onto the count/label text. */
.mpa-status-tile {
  display: block;
  width: 100%;
  border: 1px solid transparent;
  border-radius: var(--mpa-radius-sm);
  background: none;
  padding: .35rem .5rem;
  margin: -.35rem -.5rem;
  font: inherit;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
}
.mpa-status-tile:hover { background: var(--mpa-tint); }
.mpa-status-tile:focus-visible {
  outline: none;
  border-color: var(--mpa-ink-brand);
  box-shadow: 0 0 0 2px var(--mpa-ink-brand) inset;
}
.mpa-status-tile.active {
  background: var(--mpa-tint-strong);
  border-color: var(--mpa-ink-brand);
}
.mpa-status-tile.active .mpa-status-name { color: var(--mpa-ink-brand); }

/* ── Dashboard Invoice Status Hotfix V1 (29 Aug 2026) supersedes the §1
   FINAL FINAL acceptance corrective note that used to live here: the
   Dashboard Money card's status tiles now deliberately DO pick up the
   Invoice list's .mpa-inv-status-grid (a 2x2 grid, every width, see
   above) instead of keeping their own mobile-only right-aligned
   layout — Rory asked the two surfaces to read as the same product
   language. .mpa-dash-status-grid is retired; dashboard.html no longer
   references it. */

/* ── F2. Invoice list — Overdue + Term read as one filter/header area
   (28 Aug 2026): both already share one flex wrapper in the DOM, but at
   phone width they wrap onto separate lines with no visual grouping, so
   Overdue reads as an unrelated floating button. Mobile-only bordered/
   padded block (same visual language as .mpa-filter-bar, not that class
   itself — .mpa-filter-bar's `.form-label` styling is desktop-active too
   and would have re-skinned the "Term:" label everywhere that shares it).
   Desktop keeps its existing ms-lg-auto inline layout untouched. */
@media (max-width: 767.98px) {
  .mpa-inv-header-actions {
    width: 100%;
    background: var(--mpa-surface);
    border: 1px solid var(--mpa-line);
    border-radius: var(--mpa-radius);
    padding: .75rem 1rem;
  }
  .mpa-inv-header-actions > a.btn { width: 100%; }
  .mpa-inv-header-actions form { width: 100%; display: flex; align-items: center; gap: .5rem; }
  .mpa-inv-header-actions form .form-select { flex: 1 1 auto; min-width: 0; }
}

/* ── F3. Invoice list — bulk/action toolbar spacing (28 Aug 2026): a tight
   flex-wrap row let buttons bunch with inconsistent gaps (one pair inside
   #bulk-form was separated by a literal &nbsp; rather than a real flex
   gap). Mobile-only: stack every action to full width with one consistent
   gap. No action, permission, disabled state or handler changes. */
@media (max-width: 767.98px) {
  .mpa-inv-toolbar {
    flex-direction: column;
    align-items: stretch;
    gap: .6rem;
  }
  .mpa-inv-toolbar > form,
  .mpa-inv-toolbar > span { width: 100%; }
  .mpa-inv-toolbar .btn { width: 100%; }
  .mpa-inv-toolbar #bulk-form {
    display: flex;
    flex-direction: column;
    gap: .5rem;
  }
}

/* ---------------------------------------------------------------------------
   Scheduling Availability V1B (28 Aug 2026) — the unavailable-OPTION state,
   shared by every teacher/venue selector (timetable occurrence edit, series
   edit, Add to timetable, attendance Admin Controls).

   Deliberately the SAME visual language the disabled pupil/teacher options
   already used (.is-blocked's muted, not-allowed treatment), extended to
   <option> rather than invented a second time for rooms.

   ⚠ <option> is a browser-drawn control on most platforms: only colour and
   font weight are reliably honoured (padding/opacity/borders are ignored by
   Chrome's native popup on Windows and by every mobile picker). So this is
   colour only — the reason itself is carried in the option's TEXT, which is
   what actually makes the state readable everywhere, including a native
   iOS/Android wheel where no CSS applies at all. The `disabled` attribute,
   not this rule, is what makes it unselectable.
   --------------------------------------------------------------------------- */
option.mpa-opt-unavailable {
  color: var(--mpa-danger-ink);
}
option.mpa-opt-unavailable:disabled {
  color: var(--mpa-muted, #6c757d);
}
/* The under-select reason line: allowed to wrap, never nowrap-clipped into an
   invisible horizontal scroll (the 360px defect V1's own visual pass found on
   .mpa-chip.danger — same lesson, applied here before it could happen again). */
.mpa-avail-reason,
.mpa-tva-availnote {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* ══════════════════════════════════════════════════════════════════════
   MOBILE LAUNCH POLISH V1 (28 Aug 2026) — bounded responsive/UI corrective.
   Desktop is unchanged everywhere below unless a comment says otherwise;
   every rule is scoped to a max-width media query or a component already
   only used on a specific page. ══════════════════════════════════════ */

/* ── A0. Dashboard KPI internal alignment (29 Aug 2026, Rory acceptance
   pass): the OUTER grid (2x2 + full-width 5th tile) is accepted and
   untouched. Internally, the 48px icon tile + 1rem gap ate ~64px of a
   ~170px mobile card before any text even started, so a longish label
   ("NEW STUDENTS (30 DAYS)") was left with barely enough room to avoid
   wrapping onto 3 cramped lines against the card's right edge — visually
   read as "icon has room to spare, text is squeezed", though the flex
   split was technically symmetric. A smaller icon + gap at mobile widths
   only (desktop's 48px tile is unchanged) hands that freed width straight
   to the text column, which is the ONLY thing users read here. */
@media (max-width: 767.98px) {
  .mpa-kpi { padding: 1rem .9rem; gap: .7rem; }
  .mpa-kpi .mpa-icon-tile { width: 38px; height: 38px; border-radius: 11px; font-size: var(--mpa-text-lg); }
}

/* ── Shared detail-page header (29 Aug 2026, Rory FINAL acceptance
   corrective) — replaces the previous two attempts (a breadcrumb repeating
   the entity name above a duplicate H1; then sr-only-hiding that H1's
   text, which just moved the problem: the breadcrumb+H1 block had no
   min-width:0, so at phone width flex-wrap could push the WHOLE block
   onto its own line, orphaning the hamburger alone above it — exactly the
   defect Rory kept reporting).

   ROOT FIX, from partials/_detail_header.html's detail_header_text macro:
   the module line no longer repeats the entity name ("Students", not
   "Students / Chloe Ashworth"), so there is nothing left needing to be
   hidden — the H2 is real, visible, unique, at every width. And the
   hamburger + text block are flex siblings where the text block itself
   carries flex:1 + min-width:0 — the standard "icon beside growing text
   must never separate" pattern. A flex item with min-width:0 is ALLOWED
   to shrink below its content's natural width, which lets ITS OWN
   children wrap internally instead of the browser ever needing to evict
   the whole item onto a new flex-wrap line — that eviction was the actual
   mechanism stranding the hamburger. Desktop and mobile share the exact
   same markup and mostly the same CSS; only .mpa-detail-actions'
   full-width/right-aligned fallback is mobile-only (desktop has the
   room to sit inline via ms-auto instead). */
.mpa-detail-head {
  display: flex;
  align-items: flex-start;
  gap: .9rem;
  flex-wrap: wrap;
  margin-bottom: 1.6rem;
}
/* FINAL corrective (29 Aug 2026): min-width:0 on .mpa-detail-head-text
   alone did NOT stop the hamburger from separating from the title block
   at phone widths, because .mpa-detail-head itself is flex-wrap:wrap —
   and a wrapping flex container decides which line an item lands on
   using that item's UNSHRUNK hypothetical size, before flex-shrink (and
   therefore min-width:0) ever gets applied. A long name's natural
   (un-shrunk) width didn't fit next to the hamburger, so the browser
   evicted the whole text block to its own line, stranding the hamburger
   above it -- the exact bug this whole rebuild exists to fix, just
   reproduced one level up.
   Fix: hamburger + text block are grouped into their own NON-wrapping
   inner row (.mpa-detail-head-row has no flex-wrap of its own), so
   there is only ever one line to fit them on and flex-shrink always
   applies within it. That row is itself the flexible item inside the
   outer .mpa-detail-head, so the page-specific actions block can still
   wrap below it at mobile exactly as before. */
.mpa-detail-head-row {
  display: flex;
  align-items: flex-start;
  gap: .9rem;
  flex: 1 1 auto;
  min-width: 0;
}
.mpa-detail-head-text {
  flex: 1 1 auto;
  min-width: 0;
}
.mpa-detail-module {
  display: block;
  font-size: var(--mpa-text-xs);
  font-weight: 700;
  letter-spacing: var(--mpa-tracking-label);
  text-transform: uppercase;
  color: var(--mpa-muted);
  text-decoration: none;
  margin-bottom: .2rem;
}
.mpa-detail-module:hover { color: var(--mpa-ink-brand); text-decoration: underline; }
.mpa-detail-name { margin: 0; overflow-wrap: anywhere; }
.mpa-detail-subtitle { overflow-wrap: anywhere; }
.mpa-detail-actions {
  margin-left: auto;
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
}
@media (max-width: 767.98px) {
  .mpa-detail-actions {
    width: 100%;
    justify-content: flex-end;
  }
}
/* One-Click Tenant-Logo Home Navigation V1 (4 Sep 2026) — same "own
   column, row 1" trick as .mpa-page-head's canonical mobile header
   above, applied to the four detail pages (Students, Staff, Attendance
   roll_call, Invoice detail) that use .mpa-detail-head instead.
   Desktop needs NO special rule at all: .mpa-detail-head-row already
   carries flex:1 1 auto (it grows to fill free space) and
   .mpa-detail-actions already carries its own margin-left:auto -- the
   logo, placed after either, is simply pushed to the true far right by
   whichever of those two mechanisms is in play (or both to right just
   .mpa-detail-actions, if present -- see that block above), with the
   plain .mpa-detail-head gap providing the same spacing every other
   child already gets. Mobile needs the grid, same reason as
   .mpa-page-head: the plain flex-wrap here would otherwise evict the
   logo onto its own new line (or worse, the SAME line as an already-
   full-width .mpa-detail-actions block, which forces wrap regardless of
   the logo's own size), an added row this brief explicitly forbids. */
@media (max-width: 767.98px) {
  .mpa-detail-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    row-gap: .7rem;
    column-gap: .7rem;
  }
  .mpa-detail-head > .mpa-detail-head-row { grid-column: 1; grid-row: 1; }
  .mpa-detail-head > .mpa-home-logo {
    grid-column: 2; grid-row: 1;
    justify-self: end; align-self: center; margin-left: 0 !important;
  }
  /* Every other child (.mpa-detail-actions, roll_call's pay-rate block,
     invoice detail's status/actions area) -- own full-width row below,
     same "every extra child becomes a row" convention .mpa-page-head
     already established. */
  .mpa-detail-head > *:not(.mpa-detail-head-row):not(.mpa-home-logo) {
    grid-column: 1 / -1; margin-left: 0 !important;
  }
}

/* ── A1. Dashboard KPI grid — 5th (Attendance) tile orphaned at half-width
   (28 Aug 2026): the KPI row is Bootstrap's row-cols-2 at phone width (2
   items per row from a 5-item row = 2,2,1), so the last tile sat alone at
   50% width with a dead half-row gap beside it. Bootstrap's row-cols-2
   sets width:50% on every direct child with equal specificity to a bare
   class selector, loaded BEFORE app.css, so this plain nth-child override
   (no !important needed) wins on source order + specificity alone.
   Scoped to <768px, matching exactly the width range row-cols-2 is
   actually active (row-cols-md-3 takes over at md, a different remainder
   pattern this brief doesn't ask to change). */
@media (max-width: 767.98px) {
  .mpa-kpi-row > .col:nth-child(5) {
    flex: 0 0 100%;
    width: 100%;
  }
  /* UI-CONSISTENCY-1 (21 Sep 2026): the same orphan rule for ANY odd
     last tile in a two-up KPI row (Overdue's third tile), not only the
     dashboard's fifth -- one rule for the house component, not one per
     page. The nth-child(5) rule above is kept as-is for the dashboard. */
  .mpa-kpi-row > .col:nth-child(odd):last-child {
    flex: 0 0 100%;
    width: 100%;
  }
  /* The now-full-width tile keeps the SAME centred column as its four
     siblings (21 Sep 2026). It used to be left-aligned to stop the extra
     width stretching an icon+text ROW apart -- irrelevant now that the
     card is a centred stack, and actively wrong: it was the one tile in
     five that centred differently. */
}

/* ── "This term so far" KPI cards — FULLY CENTRED (21 Sep 2026) ────────
   Supersedes AI-OPS-1 §14's partial fix, which only centred the
   icon+text+arrow GROUP, and only from 1200px up. Rory has asked for
   these centred more than once, so this is the complete treatment at
   EVERY width: the card becomes a centred COLUMN, and the icon, the
   number, the label and any secondary line each sit on the card's own
   centre line.

   All five cards therefore share one geometry -- the same stack, the same
   min-height, the same centred children -- which is the point: five tiles
   in a row that each centre differently read as five different objects.

   It is also now the SAME language as the Admin Console's .aiu-kpi cards
   (partials/_ai_console.html): centred label, centred value, centred
   sub-line, equal heights. One product, one kind of stat tile.

   THE ARROW (final micro-corrective, 21 Sep 2026) is anchored at the
   card's RIGHT edge, vertically centred, OUT OF FLOW -- it used to be the
   last row of the stack, which reserved an empty line under the label on
   every card whether or not anyone was hovering. It is `display: none`
   at rest and appears only on hover/focus-visible, so it never occupies
   space, never moves the centred figure/label, and never changes the
   card's height. qa/test_ai_ops_1.py's D15 measures the centred GROUP of
   visible children at rest: a display:none arrow has no box, so the
   group is exactly the figure+label block and D15 holds honestly.

   Scoped to .mpa-kpi-row: the bare .mpa-kpi class is reused on other
   pages with a tighter horizontal layout. (Class detail joined this row
   on 24 Oct 2026, with its icon tiles: .mpa-kpi-row--tiles.) */
.mpa-kpi-row .mpa-kpi {
  position: relative;               /* the arrow anchors to the card */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: .35rem;
  /* Final visual corrective (21 Sep 2026): the icon tile is gone and the
     arrow is out of flow (micro-corrective), so the stack is figure +
     label (+ chip/note) and the card is only as tall as that. */
  min-height: 0;
  padding: .9rem 1rem;
}
/* The text block is a plain <span> wrapper; it has to fill the column so
   its own children centre on the CARD's axis, not on their own width.

   ⚠ `:not(.mpa-icon-tile)` IS THE WHOLE OF §B (21 Sep 2026). The icon tile
   is ALSO a bare <span>, so the first version of this rule stretched it
   from a 48px rounded square to the full width of the card -- the
   "full-width lavender horizontal bar" Rory rejected. The tile is not a
   text block and must never fill the column. */
.mpa-kpi-row .mpa-kpi > span:not(.mpa-icon-tile) { width: 100%; }
/* Final visual corrective (21 Sep 2026): there is no tile in this row any
   more (dashboard.html removed them); the guard above is kept so a tile
   added back by mistake can never become a full-width bar again. */
.mpa-kpi-row .mpa-kpi-value { margin-top: 0; }
.mpa-kpi-row .mpa-kpi-label { margin-top: .3rem; }
/* .mpa-kpi-label-row is display:flex, so text-align does not reach it --
   Attendance's label + RAG chip centre as one unit, chip included. */
.mpa-kpi-row .mpa-kpi-label-row { justify-content: center; }
/* The arrow: right edge, vertically centred, only while hovered/focused.
   position:absolute takes it out of the column, display:none at rest
   means it has no box at all, and the pointer-only appearance means no
   layout ever changes -- the centred content is measured identical
   hovered and unhovered (qa/pw_ai_support_v1a.py R16.1). */
.mpa-kpi-row .mpa-kpi-arrow {
  display: none; position: absolute; right: .85rem; top: 50%;
  transform: translateY(-50%); margin: 0; opacity: .85;
}
.mpa-kpi-row a.mpa-kpi:hover .mpa-kpi-arrow,
.mpa-kpi-row a.mpa-kpi:focus-visible .mpa-kpi-arrow { display: block; transform: translateY(-50%); }
/* Class detail (24 Oct 2026): the same centred stack with the icon tile on top. */
.mpa-kpi-row--tiles .mpa-kpi { height: 100%; }
.mpa-kpi-row--tiles .mpa-kpi .mpa-icon-tile { flex: 0 0 auto; }
@media (max-width: 575.98px) {
  /* A touch tighter on a phone, where two columns of these already fill
     the width -- same geometry, less air. */
  .mpa-kpi-row .mpa-kpi { min-height: 5.6rem; padding: .8rem .7rem; gap: .25rem; }
}

/* ── A2. Dashboard Quick Actions — inconsistent mobile grid (28 Aug 2026):
   .mpa-action-btn is an inline-flex chip sized to its own label, and the
   parent is a plain d-flex flex-wrap — fine on desktop (chips wrap freely
   in the open row), but at phone width each row breaks at a different x
   depending on that row's label lengths, so nothing lines up into columns
   and the final odd chip (5, 9 or 11 actions) sits alone at content width.
   A deliberate 2-column CSS grid fixes the alignment without touching
   which actions exist, their hrefs, or permissions — purely the wrapping
   container the existing unchanged <a class="mpa-action-btn"> tags sit in.
   Scoped to the admin Dashboard's own wrapper class (.mpa-quick-actions,
   added to that one div only) rather than every .mpa-action-btn wrapper
   app-wide (dashboard_teacher/portal pages use the same chip component
   but are out of this bounded corrective's scope). */
@media (max-width: 767.98px) {
  /* !important required: the wrapper also carries Bootstrap's .d-flex,
     whose display:flex is itself !important and would otherwise always
     win regardless of source order or specificity. */
  .mpa-quick-actions {
    display: grid !important;
    grid-template-columns: 1fr 1fr;
    gap: .5rem;
  }
  .mpa-quick-actions > .mpa-action-btn {
    width: 100%;
    justify-content: flex-start;
  }
}

/* ── A3. Venue Rooms V1 manual-acceptance corrective (29 Aug 2026) —
   .mpa-list/.mpa-list-row: the flex-proportioned row pattern Day-specific
   Rates and the Rooms list (venues/form.html) both use instead of a raw
   <table>, which gave one column arbitrary width and clipped the other at
   360px even inside .table-responsive (a scrollbar, never a reflow). No
   layout rules here — every row already carries its own flex-basis inline
   (the proportions differ between the two lists) — this only adds the
   subtle hover affordance a "modern list" needs, matching the existing
   row-hover language elsewhere (students/staff lists) at low specificity
   so it never fights an inline style. */
/* Rory's SECOND corrective (30 Aug 2026, §11) — root cause of the
   "curved partial underline" artefact under Day-specific Rates/Rooms
   rows: border-radius on the SAME element as border-bottom curves the
   bottom border's own corners, not just the hover background it was
   actually meant to round. Moved onto :hover only — the resting row
   (border-bottom, when present) is now a plain flat line at every
   width; hover keeps its rounded affordance exactly as before. */
.mpa-list-row {
  transition: background-color .1s ease;
}
.mpa-list-row:hover {
  background: var(--mpa-hairline);
  border-radius: var(--mpa-radius-sm);
}
/* .mpa-card-inset — a visually distinct "add new" area sitting below an
   .mpa-list (Rooms' own Add-room form), so it reads as its own step
   rather than one more row in the list above it. */
.mpa-card-inset {
  background: var(--mpa-bg);
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
}
/* Curriculum Header + Staleness Control Amendment (4 Sep 2026, §G) --
   .mpa-staleness-row: the "Warn when older than [n] months [Save]"
   control read as visually unfinished per Rory's review -- root cause
   was `mpa-field-row`'s `align-items:flex-end !important` (meant for
   multi-column pickers) fighting the plain `align-items-center` this
   single inline row actually wants (see curriculum/library.html's own
   comment). This class gives it a light bounded surface -- same
   .mpa-card-inset tint/border/radius token pair, just inline-sized
   rather than block-level -- so the control cluster reads as one
   deliberate settings row inside the EXISTING card (never a new one),
   never stretching to the card's full width (padding + flex-wrap
   handle any width, from a small inline cluster to wrapping onto two
   lines on a narrow phone card). */
.mpa-staleness-row {
  background: var(--mpa-bg);
  border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm);
  padding: .5rem .75rem;
  flex: 0 1 auto;
}

/* ── Invoice detail head (V3 polish, 2 Sep 2026) ──────────────────────
   Root cause of the "subtly inconsistent indentation" Rory saw: the
   1 Sep corrective moved the metadata into its own full-width flex row
   and REPRODUCED the hamburger column with a hard-coded
   padding-left: calc(2.5rem + .25rem + .9rem). Density V3 made the
   desktop hamburger 2.3rem, so the metadata sat 0.2rem right of the title
   column — any future hamburger/gap change would drift it again.
   Fix: at >=768px the head becomes a 3-column grid, hamburger | text |
   actions. .mpa-detail-head-row (kept for the <768px no-wrap contract)
   becomes display:contents so the hamburger and the text block are grid
   items; the title block takes grid column 2 (the metadata originally did
   too — since 8 Oct 2026 it starts on the content edge, see the rule
   below), so it aligns by construction — the same hamburger + me-1 + .9rem gap edge
   every other migrated detail page (Students, Staff) uses. The actions
   wrapper takes column 3 spanning both rows: a compact top-right toolbar
   that wraps into at most two rows instead of a full-width band under the
   metadata. Below 768px the previous stacked layout is unchanged. */
.mpa-detail-meta { flex: 0 0 100%; min-width: 0; }
.mpa-invoice-actions-wrap { display: flex; flex-direction: column; gap: .5rem; align-items: flex-end; }
.mpa-invoice-actions { display: flex; flex-wrap: wrap; gap: .5rem; justify-content: flex-end; align-items: center; }
.mpa-invoice-actions-note { flex: 0 0 100%; text-align: right; }
/* A disabled action keeps its explanation on hover/focus via this wrapper (a
   disabled button fires no events) — e.g. Resend email while the school's
   email sending is switched off. Inline on desktop; a full-width peer on
   phones, exactly like an enabled button (FREEAGENT-V1, 1 Oct 2026). */
.mpa-btn-tipwrap { display: inline-block; }
@media (min-width: 768px) {
  .mpa-invoice-head {
    display: grid;
    /* One-Click Tenant-Logo Home Navigation V1 (4 Sep 2026): 4th column
       added for .mpa-home-logo, immediately right of the actions
       toolbar column -- the header's existing far-right slot, now with
       the Home logo at the true far right of it per the brief's
       ordering ("existing controls, then the logo"). This does take a
       little width from column 2 (text/meta, still the flexible
       minmax(0,1fr) column) versus before -- the 4-peer-action-on-one-
       row contract below still holds at the same 1280px+ widths this
       was tuned for; only very tight in-between widths (768-900px-ish)
       might wrap one action sooner than before, same graceful wrap
       behaviour this toolbar already has by design. */
    /* 24 Oct 2026 (Rory, 820px): the title column keeps at least 12rem.
       With minmax(0, 1fr) the actions toolbar took every pixel at tablet
       widths and the pupil's name collapsed to one letter per line; now
       the toolbar wraps its buttons into rows beside a readable name. */
    grid-template-columns: auto minmax(12rem, 1fr) auto auto;
    column-gap: .9rem;
    row-gap: .25rem;
    align-items: start;
  }
  .mpa-invoice-head > .mpa-detail-head-row { display: contents; }
  .mpa-invoice-head .mpa-menu-btn { grid-column: 1; grid-row: 1; }
  .mpa-invoice-head .mpa-detail-head-text { grid-column: 2; grid-row: 1; }
  /* Invoice Document V1 manual acceptance (8 Oct 2026, Rory): only the
     identity block (eyebrow + title, column 2) sits beside the hamburger.
     The status/metadata row below it starts on the header's own left
     edge — spanning the hamburger and text columns. That is the main
     content edge the body cards use, the same "metadata reclaims the width
     under the hamburger" rule phones already follow
     (.mpa-head-reclaim / canonical detail head: grid-column 1 / -1). It
     stops at column 2, so the actions (3) and the logo (4) are untouched. */
  .mpa-invoice-head .mpa-detail-meta { grid-column: 1 / 3; grid-row: 2; }
  .mpa-invoice-head .mpa-invoice-actions-wrap {
    grid-column: 3; grid-row: 1 / span 2;
    justify-self: end; align-self: start;
    /* rem, not %: a percentage max-width has no definite reference inside
       an auto grid track and collapsed the toolbar to ~270px (4 buttons
       => 3 rows). The cap keeps the four approved-state peer actions on
       ONE row from 1280px up (the Invoice Payment Options V1 responsive
       contract); more actions or a narrower window wrap into a second
       compact row, never a third.
       FREEAGENT-V1 (1 Oct 2026): 42rem (546px @13px) fitted the PROD
       labels ("Publish invoice | Publish & email") but not the provider
       ones — "Send to FreeAgent (draft)" makes the four ~560px, so "Mark as
       paid" wrapped at every desktop width. 46rem (598px) fits them with
       room to spare; it is only a cap, so shorter rows are unchanged. */
    max-width: 46rem;
  }
  .mpa-invoice-head > .mpa-home-logo {
    grid-column: 4; grid-row: 1;
    justify-self: end; align-self: start; margin-left: 0 !important;
  }
  /* Mark-as-paid panel drops down over the page instead of growing the
     header (the toolbar must never become a band). */
  .mpa-invoice-actions .mpa-markpaid { position: relative; }
  .mpa-invoice-actions .mpa-markpaid > .card { position: absolute; right: 0; top: 100%; z-index: 1030; text-align: left; }
}
/* Marketing Website V1.2 final acceptance (13 Oct 2026, Rory): a published,
   unpaid invoice has FIVE peer actions (Resend email | Download invoice |
   Correct invoice… | Void invoice… | Mark as paid, ~660px), so at 46rem
   (598px) "Mark as paid" wrapped alone under the others at EVERY desktop
   width, 1920 included — the same defect FREEAGENT-V1 fixed for the
   approved state. Where the window has room (1280px+, the width the
   one-row contract above is tuned for) the cap is 52rem (676px): the five
   fit on one row and the title column keeps 470px+ at 1280. Below 1280
   the 46rem cap and its graceful second row are unchanged. */
@media (min-width: 1280px) {
  .mpa-invoice-head .mpa-invoice-actions-wrap { max-width: 52rem; }
}
@media (max-width: 767.98px) {
  /* The peer actions stack as one deliberate column of full-width,
     equal-height buttons instead of a ragged wrap (1 Sep 2026 contract,
     unchanged). */
  .mpa-invoice-actions {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: .5rem;
    width: 100%;
  }
  .mpa-invoice-actions-wrap { width: 100%; }
  .mpa-invoice-actions > form,
  .mpa-invoice-actions > div { min-width: 0; }
  .mpa-invoice-actions > form > .btn,
  .mpa-invoice-actions > div > .btn,
  .mpa-invoice-actions > a.btn,
  .mpa-invoice-actions > form > .mpa-btn-tipwrap > .btn { width: 100%; }
  .mpa-invoice-actions > form > .mpa-btn-tipwrap { display: block; }
  /* INVOICE-MOBILE-POLISH-1 FINAL ACCEPTANCE (19 Sep 2026, §5): the
     accounting-software helper note (.mpa-invoice-actions-note, a direct
     child of this same grid) inherited the desktop rule's
     `text-align: right` two rules up -- harmless when it was one short
     line trailing a right-justified FLEX row, but here the grid item is
     blockified to the full single-column width (same width the buttons
     above stretch to), so right-aligned text read as oddly indented/
     off-centre under a column of full-width, left-reading buttons. Left
     align only, same grid column/width as the buttons -- no separate
     max-width or indent, so its left edge already matches theirs. */
  .mpa-invoice-actions-note {
    text-align: left;
    line-height: 1.35;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   DESKTOP DENSITY + RESPONSIVE CONTRACT V2 (1 Sep 2026) + V3 (2 Sep 2026)
   Replaces the rejected V1 block (V1 only trimmed chrome and left the
   16px root untouched — Rory found the app still materially oversized).

   V3 — MEASURED 100% PARITY (2 Sep 2026). Playwright measured V2@100%
   against the pre-V2 app at 82.5% browser zoom and found them ALREADY
   equal (document heights within 0-10% on every admin page, 1440x900).
   Rory still zooms PROD (= V2) to 80-85%, so the reference he actually
   prefers is V2 itself at ~85%: body 11.9px, buttons/inputs 28-31px,
   page title ~21px, gutters ~18px, table rows ~36px, line-height ~17.8px.
   Three things stopped V2 from reaching that at 100%, none of them the
   root alone:
   (a) Bootstrap's control geometry is NOT root-scaled linearly: .btn,
       .form-control, .form-select, .input-group-text all carry a literal
       line-height 1.5 and .375rem vertical padding, so a control is
       1.5em + .75rem + 2px — at a 14px root that is 33.5px against a
       28.5px reference, and the app's own .mpa-field/.mpa-select
       min-heights were pinned in px at 36 (reference 30.6).
   (b) Body line-height 1.5 (--bs-body-line-height) plus rem spacing
       tokens keep every text block, table row and card taller than the
       reference even where the font is right.
   (c) 29 legacy page titles (<h2 class="mb-0 fw-bold">, e.g. Add class,
       Import, confirm pages) never had V2's .mpa-page-title trim, so they
       rendered 28px next to 24.5px migrated titles.
   V3 therefore: 13px root (body copy stays >= 13px — Rory is NOT asking
   for tiny text; the reference's 11.9px body is deliberately not
   matched), body line-height 1.45, Bootstrap control tokens brought to
   the reference geometry (.3rem padding-y, line-height 1.45 => 28.7px
   controls), app control min-heights 30/28px, gutters/title/head-gap/
   card/KPI/table paddings re-trimmed to the reference numbers, the
   legacy titles unified onto the same 1.6rem as .mpa-page-title, and a
   10.5px legibility floor extended to every micro-label that would have
   dropped under 10px at the new root. Everything below 992px is
   untouched (mobile metrics byte-identical to V2, asserted by
   qa/test_desktop_density_v3.py). No transform/zoom anywhere.

   ROOT CAUSES this block addresses, all system-level:
   (1) Desktop scale. Every rem in this file AND in Bootstrap keys off the
       16px root at every width, so at 100% zoom a 1440–1920px monitor got
       the same absolute sizes as a tablet. Rory's "looks right at ~80–85%
       zoom" is exactly a root-scale statement. V2 sets the ROOT to 14px at
       >=992px (×0.875 on every rem-sized thing: type, spacing utilities,
       buttons, inputs, cards, tables, modals) and then trims the handful
       of px-fixed / deliberately roomy components (icon tiles, control
       min-heights, drawer, page title, card/KPI padding) on top — the two
       together land near the 0.8–0.85 target while body copy stays 14px
       and the smallest uppercase micro-labels get a 10.5px floor. This is
       NOT zoom/transform: nothing is rasterised, hit-testing and fixed
       elements are exact, and every mobile rule below 992px is untouched.
   (2) Nav drawer scrolled at 1366x768 because header + 16 links + account
       block summed past the viewport. Fixed by density, plus a
       height-sensitive compact treatment (max-height:720px) so short
       laptop viewports never grow a scrollbar — no overflow:hidden.
   (3) Mobile page headers: .mpa-page-head is flex-wrap and every child
       (hamburger, title, actions, term form) wraps INDEPENDENTLY, so a
       wide child evicts the title under the hamburger. Below 768px the
       head becomes a 2-column GRID: hamburger | title on row 1, every
       other child a full-width row beneath — one contract, no template
       forks. Term controls in the head get one shared field surface.
   (4) Filter bars built from several small forms (Reports) wrapped
       label/control mid-row on phones. Those forms become label|control
       grids sharing the same 5.2rem label column the single-form bars
       (Students/Staff/Classes) already stack to.
   (5) Settings' two nav tiers wrapped as unrelated pills. Both tiers are
       now deterministic grids on phones (3 equal group cells; 2-column
       sub-tabs with an odd last item spanning).
   (6) Desktop file tooling on phones. .mpa-file-action is the single,
       greppable policy class: Excel/XLSX export, Bulk import, Bulk export
       are display:none below 768px everywhere (product decision — the
       endpoints and desktop buttons are untouched, no mobile overflow
       menu). ══════════════════════════════════════════════════════════ */

/* ── 0. Base (mobile/tablet) drawer width — was an inline style on
   #appSidebar in _sidebar.html; kept here ABOVE the desktop override so
   equal-specificity source order resolves the way it reads. ── */
#appSidebar { width: 264px; }

/* ── 1. Mobile file-action policy ── */
@media (max-width: 767.98px) {
  .mpa-file-action { display: none !important; }
}

/* ── 1a. Dense report tables become labelled records on phones/tablets
   (PWA Responsive UI Consistency Polish V1, §13F/13I): Revenue by class,
   Class Performance, Attendance by-class and Staff Costs' per-teacher
   table all share the exact same shape -- a Class/Teacher identity column
   plus 4-5 numeric metric columns, table-layout:fixed with % widths meant
   for a desktop-width row. Squeezed into a phone that's either a
   horizontal-scroll table or unreadably tiny text; opt-in .mpa-table-stack
   is the shared fix (same SAME <td> markup and server-rendered values,
   just re-flowed -- no second calculation, no duplicated row data): the
   first column becomes the card's bold title line (the brief's own
   "Dinkie – Ballet" example), every other column becomes a label:value
   row via its own data-label attribute + the CSS-generated content below.
   Desktop (>=992px) is completely untouched -- this only exists <992px,
   the same dense-table breakpoint the freeze-cap/first-column rules
   above already use. */
@media (max-width: 991.98px) {
  .mpa-table-stack thead { display: none; }
  .mpa-table-stack, .mpa-table-stack tbody, .mpa-table-stack tr, .mpa-table-stack td { display: block; width: 100%; }
  /* Final Forensic Corrective Pass (4 Sep 2026, §25): a stacked record is
     a hairline-separated ROW inside the .mpa-card that already frames the
     table -- not its own bordered/rounded card (that was the "box inside
     a box inside a box" Rory kept flagging on Attendance by-class, the
     comparison tables and the per-teacher breakdowns). ONE container
     level: card outside, separators inside. */
  .mpa-table-stack tr {
    background: var(--mpa-surface); border: none; border-top: 1px solid var(--mpa-hairline);
    border-radius: 0; margin-bottom: 0; padding: .65rem 1rem;
  }
  .mpa-table-stack tbody tr:first-child { border-top: none; }
  .mpa-table-stack tfoot tr { border-top: 2px solid var(--mpa-line); }
  .mpa-table-stack tbody tr:last-child { margin-bottom: 0; }
  .mpa-table-stack.table-hover tbody tr:hover { background: var(--mpa-surface); }
  /* Real-device corrective pass 2 (grey "spreadsheet cell" fix): PROVEN
     root cause, not assumed -- Bootstrap's own .table rule
     (`.table > :not(caption) > * > *`) paints EVERY cell with
     background-color:var(--bs-table-bg), and that variable is never
     independently set in this app, so it inherits --bs-body-bg, which
     this app points at --mpa-bg (app.css :root) -- the pale lavender
     PAGE canvas colour, not white. Only the sticky-first-column-freeze
     rule (above, .mpa-table.table tbody td:first-child) happened to
     override it back to white on the FIRST cell alone (an unrelated
     feature for the desktop horizontal-scroll pattern), which is exactly
     why only the title line read as a clean white card and every metric
     row below it read as a distinct grey block -- confirmed by measuring
     the two cells' computed background-color directly (255,255,255 vs
     247,246,250) before this fix. transparent lets the tr's own white
     background (above) show through every cell uniformly, so a
     table-stack card reads as ONE clean white Solis card, not a
     white title strip sitting on a grey table body. */
  .mpa-table-stack td {
    border: none !important; padding: .2rem 0 !important; text-align: left !important;
    white-space: normal !important; overflow-wrap: anywhere; text-overflow: clip;
    background: transparent !important;
  }
  .mpa-table-stack td:first-child { font-weight: 700; font-size: var(--mpa-text-base); padding-bottom: .4rem !important; }
  /* Final Forensic Corrective Pass (4 Sep 2026, §13 root cause, measured):
     this label:value row had NO flex-wrap, so a full-width child (the
     Revenue share bar's flex-basis:100%) forced the VALUE text down to its
     min-content width -- and with overflow-wrap:anywhere inherited from
     the td rule above, min-content for "£3,630.00" is ONE CHARACTER, which
     is exactly the "£3,6 / 30. / 00" vertical digit split on Rory's real
     iPhone (reproduced: 4 line boxes per amount at 390px before this
     fix). flex-wrap lets the bar take its own line as its comment always
     claimed, and the value is a normal-breaking (never mid-token) item
     that keeps its natural width. */
  .mpa-table-stack td:not(:first-child) {
    display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
    gap: .15rem .75rem; overflow-wrap: normal; word-break: normal;
  }
  .mpa-table-stack td:not(:first-child)[data-label]::before {
    content: attr(data-label); font-weight: 600; color: var(--mpa-muted);
    font-size: var(--mpa-text-xs); text-transform: uppercase; letter-spacing: .05em; flex-shrink: 0;
  }
  .mpa-table-stack td:not(:first-child) > * { max-width: 100%; }
  /* Revenue by class's share bar is a visual width indicator sized against
     the desktop column it lives in -- in a label:value row it has no
     comparable width to measure against, so it drops to its own full-width
     line under the figure instead of squeezing into the flex row. */
  .mpa-table-stack .mpa-share-bar { flex-basis: 100%; order: 3; margin-top: .2rem; }
}

/* ── 1b. Report KPI cards on phones (PWA Responsive UI Consistency Polish
   V1, §13B): root cause was that .mpa-kpi/.mpa-icon-tile only ever had ONE
   size (desktop's, 48px icon + 1.2rem padding) below the >=992px density
   scale that shrinks them for actual desktop — so a phone's .col-6 half-
   width card (Revenue/Growth/Attendance/Staff Costs all share the exact
   same col-6 col-xl-3 / .mpa-kpi markup) was left with barely 60px of
   usable width for a 3-line "£480.00 / GROSS REVENUE / before any costs"
   stack, which is what crowded/collided. One shared fix for every tab's
   KPI row, not a per-page hack. Icon shrinks, the text column is allowed
   to actually use the flex item's shrink (min-width:0 -- it had none, so
   long values pushed the card wider instead of wrapping), and the arrow
   (a hover-only affordance, meaningless on a touch card) stops eating
   width it isn't using. Desktop (>=992px) keeps its own already-correct
   density-scale sizing below, untouched. */
@media (max-width: 767.98px) {
  .mpa-kpi { padding: .7rem .8rem; gap: .55rem; }
  .mpa-kpi .mpa-icon-tile { width: 32px; height: 32px; border-radius: 9px; font-size: var(--mpa-text-base); }
  .mpa-kpi > span:last-child { min-width: 0; flex: 1 1 auto; }
  .mpa-kpi-value { font-size: 1.4rem; overflow-wrap: anywhere; }
  /* Real-device Reports amendment (forensic fix): overflow-wrap:anywhere
     is exactly what produced Rory's "£22,234.\n00" -- it's a valid last
     resort for a plain number (above) but a currency amount is ONE
     semantic token that must never break internally, at any width, on
     any engine. The real fix is the KPI row now being col-12 col-md-6
     below tablet width (reports/index.html) -- full card width removes
     the need to break at all; this is the belt to that braces. */
  .mpa-kpi-value.money {
    font-size: var(--mpa-text-lg); overflow-wrap: normal; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; max-width: 100%;
  }
  .mpa-kpi-label { font-size: var(--mpa-text-2xs); letter-spacing: .06em; }
  .mpa-kpi-note { font-size: var(--mpa-text-2xs); }
  .mpa-kpi-arrow { display: none; }
}

/* ── 2. Desktop density scale (>=992px only) ── */
@media (min-width: 992px) {
  html { font-size: 13px; }
  /* V3: the reference's vertical rhythm — Bootstrap body line-height is a
     root token, so one declaration reaches every text block, table row
     and list item. Controls carry their own literal line-height (below). */
  :root { --bs-body-line-height: 1.45; }

  /* Page frame */
  .main-content { padding: 1.4rem; padding-bottom: 6rem; }
  .mpa-page-title { font-size: var(--mpa-text-xl); }
  /* V3: legacy page titles (Add class, Import, confirm pages …) — plain
     Bootstrap h2 + fw-bold, never migrated onto .mpa-page-title — get the
     SAME size, so a title is one size on every page. The :not() guards
     keep in-card h2.h5/h2.h6 section headings on their own tokens. */
  main h2.fw-bold:not(.mpa-page-title):not(.h5):not(.h6) { font-size: var(--mpa-text-xl); }
  .mpa-page-head, .mpa-detail-head { margin-bottom: var(--mpa-space-block); }
  /* Dashboard header tightening (Rory UX follow-up, 10 Sep 2026;
     SUPERSEDED same day by Rory's second corrective below — kept for the
     margin only, the "empty second line" it originally described no
     longer exists). The current-term date range no longer lives in
     .mpa-page-head at all — it moved onto the "Needs attention" row
     itself (.mpa-dashboard-attention-row, dashboard.html + this file,
     above) per Rory's explicit request that the two share one row
     (heading+count left, date right). .mpa-dashboard-head's own header
     (hamburger/title/greeting/term pill) is now a plain one-line row on
     desktop with nothing below it, so this smaller margin just keeps the
     gap to the Needs Attention row (which sits right underneath) tight,
     matching every other page's page-head-to-content rhythm. Scoped to
     ONLY the Dashboard's own header wrapper (.mpa-dashboard-head,
     dashboard.html only), never the shared .mpa-page-head class every
     other page also uses, and only at this same >=992px "normal desktop
     widths" breakpoint -- mobile keeps the base spacing untouched, per
     its own already-established rules below. */
  .mpa-dashboard-head { margin-bottom: .45rem; }
  .mpa-menu-btn { width: 2.3rem; height: 2.3rem; }

  /* Micro-label legibility floor: at a 14px root the smallest uppercase
     labels would drop under 10px; max() keeps them readable without
     touching their rem tokens anywhere else. */
  .mpa-card-title,
  .mpa-kpi-note,
  .mpa-field-label,
  .mpa-section-label,
  .mpa-grid tbody th,
  .mpa-pl-more-summary,
  .mpa-detail-module { font-size: max(var(--mpa-text-xs), 10.5px); }
  .mpa-kpi-label,
  .mpa-chip,
  .mpa-sheet .form-label { font-size: max(var(--mpa-text-xs), 10.5px); }
  .mpa-filter-bar .form-label { font-size: max(var(--mpa-text-xs), 10.5px); }
  .mpa-stat-label,
  .mpa-status-name,
  .mpa-grid thead th { font-size: max(var(--mpa-text-xs), 10.5px); }
  .mpa-link, .mpa-row-side, .mpa-combo-empty { font-size: max(var(--mpa-text-sm), 10.5px); }
  .mpa-daypill, .mpa-stat-note { font-size: max(var(--mpa-text-sm), 10.5px); }
  .mpa-row-chev { font-size: max(var(--mpa-text-sm), 10.5px); }
  .badge { --bs-badge-font-size: max(.75em, 10px); }
  .mpa-table.table thead th { font-size: max(var(--mpa-text-xs), 10.5px); padding: .45rem .65rem; }
  .mpa-table.table { font-size: var(--mpa-text-base); }
  /* UI-CONSISTENCY-1: legacy .table follows the house desktop body size
     and heading floor too, so a raw table and a .mpa-table are the same
     type at every width, not just below 992px. */
  .table { font-size: var(--mpa-text-base); }
  .table > thead > tr > th { font-size: max(var(--mpa-text-xs), 10.5px); }
  .mpa-table.table tbody td { padding: .45rem .65rem; }

  /* Cards / KPI tiles */
  .mpa-card-head { padding: .7rem 1rem .5rem; }
  .mpa-card-body { padding: .3rem 1rem .85rem; }
  .mpa-kpi { padding: .8rem .9rem; gap: .7rem; }
  .mpa-kpi-value { font-size: 1.75rem; }
  .mpa-kpi-value.money { font-size: 1.35rem; }
  .mpa-icon-tile { width: 34px; height: 34px; border-radius: 10px; font-size: 1rem; }
  .mpa-icon-tile.sm { width: 26px; height: 26px; font-size: var(--mpa-text-md); }
  .mpa-action-btn { padding: .35rem .7rem .35rem .4rem; gap: .45rem; }

  /* Controls (V3): Bootstrap's .btn/.form-control/.form-select/.input-
     group-text are 1.5em + .75rem + 2px tall regardless of root — the
     literal line-height/padding is what kept them at 33.5px. Reference is
     28.5px: .3rem padding-y + line-height 1.45 at 13px = 28.7px. The app's
     own tiers follow: 42px -> 30px standard, 33px -> 28px compact.
     Desktop/mouse only; the <576px rules keep 44px/40px touch targets. */
  .btn { --bs-btn-padding-y: .3rem; --bs-btn-line-height: 1.45; }
  .btn-sm { --bs-btn-padding-y: .22rem; }
  .form-control, .form-select, .input-group-text {
    padding-top: .3rem; padding-bottom: .3rem; line-height: 1.45;
  }
  .form-label { margin-bottom: .35rem; }
  .mpa-select,
  .mpa-field,
  input.mpa-field,
  select.mpa-field,
  textarea.mpa-field,
  .mpa-pl-drawer .mpa-pl-control { min-height: 30px; }
  .mpa-field-sm,
  input.mpa-field-sm,
  select.mpa-field-sm { min-height: 28px; height: 28px; }
  .btn.mpa-field-sm { min-height: 28px; height: 28px; }
  .mpa-filter-bar { padding: .5rem .75rem; margin-bottom: .8rem; }
  .mpa-filter-bar .form-select-sm,
  .mpa-filter-bar .form-control-sm,
  .mpa-filter-bar .mpa-select,
  .mpa-filter-bar .btn-sm { height: 28px; min-height: 28px; }

  /* Nav drawer: narrower panel, denser links, tighter header/account so
     the whole drawer fits a 768px-tall viewport with room to spare. */
  #appSidebar { width: 232px; }
  .brand-nav .offcanvas-header { padding: .8rem .9rem .7rem; }
  .brand-nav .drawer-logo img { height: 28px; }
  .brand-nav .nav { padding: .4rem .6rem; row-gap: 1px; }
  /* V3 polish (Rory, 2 Sep 2026): the drawer read a touch small next to
     the V3 content — nav text .875rem->.93rem (11.4->12.1px) and row
     padding .4->.44rem (28.3->30.3px rows, +7%; the 1rem icon's line box
     sets the row, not the text). Width stays 232px; 16 links + header +
     account = ~657px, still inside 768px tall with no scrollbar. */
  .brand-nav .nav-link { padding: .44rem .7rem .44rem .85rem; font-size: var(--mpa-text-base); }
  .brand-nav .nav-link i { width: 1.4rem; margin-right: .4rem; font-size: 1rem; }
  .brand-nav .drawer-account { padding: .75rem .9rem calc(.75rem + env(safe-area-inset-bottom, 0px)); }
  .brand-nav .drawer-user { margin-bottom: .6rem; }
  .brand-nav .drawer-avatar { width: 32px; height: 32px; font-size: var(--mpa-text-sm); }
  .brand-nav .drawer-signout { padding: .4rem; }
}
/* Height-sensitive compact drawer (short laptop viewports, e.g. 1280x720):
   the same content, tighter vertical rhythm — never overflow:hidden. */
@media (min-width: 992px) and (max-height: 720px) {
  .brand-nav .offcanvas-header { padding: .55rem .9rem .5rem; }
  .brand-nav .nav { padding: .25rem .6rem; row-gap: 0; }
  .brand-nav .nav-link { padding: .3rem .7rem .3rem .85rem; }
  .brand-nav .drawer-account { padding: .55rem .9rem calc(.55rem + env(safe-area-inset-bottom, 0px)); }
  .brand-nav .drawer-user { margin-bottom: .4rem; }
}

/* ── 3. Canonical mobile admin page header (<768px) ──
   Row 1 is ALWAYS hamburger | title (grid-row:1 pins them together no
   matter how wide the title is — a long "Curriculum library" wraps inside
   its own cell instead of being evicted). Every other direct child —
   subtitle-bearing title block aside — becomes a full-width row under it:
   action groups, lone CTAs, term forms, term pills. Pages that already
   group hamburger+title in .mpa-head-row (Staff, Report Cards) get the
   identical result because that row spans both columns on row 1.

   One-Click Tenant-Logo Home Navigation V1 (4 Sep 2026) — a THIRD column
   (auto-width, sized to the logo) was added so .mpa-home-logo can also
   share row 1 without becoming its own full-width row (which the
   `.mpa-page-head > *` catch-all below would otherwise do to it, same
   as every other extra child) and without stealing column 2 from the
   title (which would visually collide the two). .mpa-head-row's own
   span was narrowed from "every column" to just columns 1-2 for the
   same reason — it used to safely span "everything" because there was
   nothing else on row 1 to collide with; now there is. */
@media (max-width: 767.98px) {
  .mpa-page-head {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: .7rem;
    row-gap: .7rem;
    margin-bottom: 1.1rem;
  }
  .mpa-page-head > * { grid-column: 1 / -1; min-width: 0; margin-left: 0 !important; }
  .mpa-page-head > .mpa-menu-btn { grid-column: 1; grid-row: 1; margin-right: 0 !important; }
  .mpa-page-head > .mpa-menu-btn + * { grid-column: 2; grid-row: 1; }
  .mpa-page-head > .mpa-head-row { grid-column: 1 / 3; grid-row: 1; }
  /* Own column, row 1 — never the "every other child becomes a
     full-width row" fate the catch-all above gives everything else,
     and never sharing column 2 with the title. */
  .mpa-page-head > .mpa-home-logo {
    grid-column: 3; grid-row: 1;
    justify-self: end; margin-left: 0 !important;
  }
  /* The hamburger centres on the TITLE LINE, not on the whole title block:
     pages with a subtitle (Class Plans, Curriculum, Staff) otherwise put
     the hamburger's centre between title and subtitle. Top-aligned row +
     a negative offset of half the (title line-height − button height)
     difference lands the button's centre exactly on the h2's centre for
     every page, subtitle or not. */
  .mpa-page-head, .mpa-page-head .mpa-head-row { align-items: start; }
  .mpa-page-head .mpa-menu-btn { align-self: start; margin-top: calc((1.5rem * 1.2 - 2.5rem) / 2); }
  /* Mobile Logo/Title Alignment Amendment (4 Sep 2026, §C): the logo used
     align-self:center, which centres it against the grid ROW's own
     computed height -- driven by the TALLEST participant, the 40px
     hamburger box, NOT the 28.8px title line hamburger/title actually
     share a centre on (via the calculated offset above). Measured before
     this fix on real DEV pages (see the amendment section of
     HANDOFF_dashboard_attendance_tenant_logo_v1.md): logo centre sat
     ~5.6-20px below the title/hamburger's shared centre, exactly Rory's
     "logo sits lower than the title" iPhone screenshot report. Same
     top-align + calculated-offset technique as the hamburger, using the
     logo's OWN box height (28px image + .4rem/6.4px vertical padding =
     34.4px, .mpa-home-logo's own declared sizing above) instead of the
     hamburger's -- lands the logo's centre on the exact same point. */
  .mpa-page-head > .mpa-home-logo {
    align-self: start;
    margin-top: calc((1.5rem * 1.2 - (28px + .4rem)) / 2);
  }
  .mpa-page-head .mpa-page-title { font-size: 1.5rem; line-height: 1.2; overflow-wrap: anywhere; }
  .mpa-page-head .mpa-greeting-inline { font-size: var(--mpa-text-base); }
  .mpa-page-head .mpa-page-sub { margin-top: .15rem; }
  /* Lone CTA / action groups: left-aligned full-width row, natural button
     widths (opt-in .mpa-head-actions groups keep their equal-split rule). */
  .mpa-page-head > .btn { justify-self: start; }
  .mpa-page-head > .d-flex,
  .mpa-page-head > .mpa-head-actions { width: 100%; }
  /* Term controls in the head: ONE shared field surface — label above,
     select full width, never truncating the selected term. */
  .mpa-page-head > form:has(select[name="term_id"]),
  .mpa-page-head > .mpa-term-block:has(select),
  .mpa-page-head > .mpa-inv-header-actions {
    width: 100%;
    background: var(--mpa-surface);
    border: 1px solid var(--mpa-line);
    border-radius: var(--mpa-radius-sm);
    padding: .55rem .8rem;
    box-shadow: none;
  }
  .mpa-page-head form:has(select[name="term_id"]) {
    display: flex; flex-direction: column; align-items: stretch; gap: .3rem; width: 100%;
  }
  .mpa-page-head form:has(select[name="term_id"]) .form-label {
    font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
    color: var(--mpa-muted); margin: 0;
  }
  .mpa-page-head form select[name="term_id"] {
    width: 100% !important; max-width: 100% !important; min-width: 0 !important;
  }
  .mpa-page-head .mpa-term-block { align-items: stretch; gap: .3rem; }

  /* ── 4. Filter bars made of several small forms (Reports): each form is
     a label | control grid sharing the 5.2rem label column the single-form
     bars already use, so FROM / TO / TERM / COMPARE all align; the apply/
     clear controls sit under the control column. ── */
  .mpa-filter-bar { padding: .75rem .85rem; gap: .6rem; }
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) {
    display: grid; grid-template-columns: 5.2rem minmax(0, 1fr);
    align-items: center; gap: .5rem .6rem; width: 100%;
  }
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) > .form-label { width: auto; }
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) > .form-select,
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) > .form-control,
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) > .mpa-select {
    grid-column: 2; width: 100% !important; min-width: 0 !important;
  }
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) > .btn,
  .mpa-filter-bar > form:not(:has(.mpa-filter-field)) > .mpa-filter-actions { grid-column: 2; justify-self: start; }
  .mpa-filter-bar .mpa-filter-actions { display: flex; gap: .5rem; }

  /* ── 5. Settings navigation: deterministic grids ── */
  .mpa-tabs.settings-group-nav {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 3px; width: 100%;
  }
  .mpa-tabs.settings-group-nav .nav-item { display: contents; }
  .mpa-tabs.settings-group-nav .nav-link {
    display: flex; align-items: center; justify-content: center; text-align: center;
    min-height: 44px; padding: .4rem .45rem; font-size: var(--mpa-text-sm); line-height: 1.2; white-space: normal;
  }
  .mpa-subtabs { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border-bottom: none; }
  .mpa-subtabs .nav-item { display: contents; }
  .mpa-subtabs .nav-link {
    display: flex; align-items: center; justify-content: center; text-align: center;
    min-height: 44px; padding: .5rem .6rem; line-height: 1.2; white-space: normal;
    border-bottom: 2px solid var(--mpa-line); margin-bottom: 0;
  }
  .mpa-subtabs .nav-link.active { border-bottom-color: var(--mpa-action); }
  .mpa-subtabs .nav-item:last-child:nth-child(odd) .nav-link { grid-column: 1 / -1; }

  /* ── 6. Curriculum library "Warn when older than [n] months [Save]"
     (PWA Responsive UI Consistency Polish V1): the label wraps to its own
     full-width line so it never fights the number field for room, then
     the number/"months"/Save group flows together underneath it, e.g.
       Warn when older than
       [ 12 ] months    [ Save ]
     Desktop is untouched -- this rule only exists below 768px. ── */
  .mpa-staleness-form { flex-wrap: wrap; row-gap: .4rem; }
  .mpa-staleness-form label { flex: 0 0 100%; }
  .mpa-staleness-form input#staleness-months { width: 4.6rem; flex-shrink: 0; }
}

/* One-Click Tenant-Logo Home Navigation V1 (4 Sep 2026) -- tablet/small-
   laptop squeeze guard, 768-1279.98px (widened from an initial
   768-1023.98px cut during QA: Timetable's own cluster -- "Schedule a
   class"/"Add private lesson" + bulk import/export + term-block -- was
   still wide enough to trip the same wrap-to-left defect at 1024px,
   this build's own explicitly required 1024x768 check). FOUND DURING QA
   (qa/test_dashboard_tenant_logo_v1.py):
   a handful of busy pages carry a right-side control cluster wide enough
   on its own at this width (Students'/Staff's bulk import+export actions,
   Timetable's "Schedule a class"/"Add private lesson" buttons plus its
   own bulk import/export, Class Plans' term-select form) that, on the
   plain flex-wrap row, appending the logo AFTER that cluster could tip
   the row over the available width -- the logo alone then wraps to a
   fresh flex line, where a small non-auto margin left it stranded near
   the LEFT edge instead of the right (proven by the suite: e.g. Students
   at 768px put the logo at left:41.6 instead of flush right).

   Fix: the SAME "hamburger + title share row 1, logo gets its own
   reserved column on row 1 too, every other child becomes its own
   full-width row beneath" grid the <768px canonical header above
   already uses -- just re-scoped to this width band too, so the logo
   can never again be pushed off row 1 by how wide any OTHER control on
   the page happens to be. Deliberately a SEPARATE, minimal block (not
   widening the <768px block itself): that block's term-form/title-size
   rules are phone-specific and were never intended for tablet width --
   only the grid MECHANICS are reused here.

   Side effect, accepted deliberately: a page whose right-side control
   already fit comfortably inline at 768-1023px before this build (e.g.
   Reports' term pill, Attendance's/Invoices' term selector) now also
   drops to its own full-width row at this width, same as it already
   does below 768px. Nothing breaks -- it's the exact same, already-
   shipped, already-tested pattern, just applied one width tier higher
   for every page uniformly, rather than hand-auditing which specific
   pages' clusters are "wide enough" to need it (fragile, and would need
   re-checking every time a page's header content changes). */
@media (min-width: 768px) and (max-width: 1279.98px) {
  .mpa-page-head {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: .9rem;
    row-gap: .6rem;
  }
  .mpa-page-head > * { grid-column: 1 / -1; min-width: 0; margin-left: 0 !important; }
  .mpa-page-head > .mpa-menu-btn { grid-column: 1; grid-row: 1; margin-right: 0 !important; }
  .mpa-page-head > .mpa-menu-btn + * { grid-column: 2; grid-row: 1; }
  .mpa-page-head > .mpa-head-row { grid-column: 1 / 3; grid-row: 1; }
  .mpa-page-head > .mpa-home-logo {
    grid-column: 3; grid-row: 1;
    justify-self: end; margin-left: 0 !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   FINAL FORENSIC RESPONSIVE/UI CORRECTIVE PASS (4 Sep 2026)
   Shared primitives added in this pass. Each is a product-level component
   reused across Reports/Invoices rather than a page patch -- see
   HANDOFF_dashboard_attendance_tenant_logo_v1.md (final corrective pass
   section) for the measured before/after evidence behind each one.
   ══════════════════════════════════════════════════════════════════════ */

/* ── §4B. Compact numeric control (Curriculum "Warn when older than [n]
   months"): sized for a 1-3 digit value plus the native spinner, never a
   generic text-input width. ── */
.mpa-num-compact { width: 4.6rem; min-width: 4.6rem; padding-right: .25rem; font-variant-numeric: tabular-nums; }
@media (max-width: 767.98px) { .mpa-num-compact { width: 5.2rem; min-width: 5.2rem; } }

/* ── §15. Phone 2x2 KPI grid (Reports "This term's totals"). The earlier
   col-12 stacking existed only because a col-6 card could split
   "£22,234.00" -- the real fix is the value being ONE nowrap token (the
   .mpa-kpi-value.money rule above) plus a compact icon/padding so the
   figure has ~110px+ of room even at 375px (measured). Cards stay
   equal-height per row. ── */
@media (max-width: 767.98px) {
  .mpa-kpi-grid-2 > [class*="col-"] { flex: 0 0 50%; max-width: 50%; }
  .mpa-kpi-grid-2 .mpa-kpi { padding: .65rem .65rem; gap: .5rem; align-items: center; }
  .mpa-kpi-grid-2 .mpa-kpi .mpa-icon-tile { width: 28px; height: 28px; border-radius: 8px; font-size: var(--mpa-text-md); }
  .mpa-kpi-grid-2 .mpa-kpi-value.money { font-size: var(--mpa-text-lg); letter-spacing: -.02em; }
  .mpa-kpi-grid-2 .mpa-kpi-label { font-size: var(--mpa-text-2xs); letter-spacing: .05em; line-height: 1.2; }
  .mpa-kpi-grid-2 .mpa-kpi-note { font-size: var(--mpa-text-2xs); }
}

/* ── §19. Segmented control that fills its container on phones (Reports
   Attendance "By class / By student"): each option takes an equal share
   instead of leaving two-thirds of the row dead. Desktop keeps the compact
   inline-flex pill unchanged. ── */
@media (max-width: 767.98px) {
  .mpa-tabs.mpa-tabs-fill { display: flex; width: 100%; }
  .mpa-tabs.mpa-tabs-fill .nav-item { flex: 1 1 0; min-width: 0; display: flex; }
  .mpa-tabs.mpa-tabs-fill .nav-link { flex: 1 1 0; width: 100%; text-align: center; justify-content: center; }
}

/* ── §13/§17/§20. Mobile record list -- THE one phone presentation for a
   dense report table: ONE container level (the enclosing .mpa-card) with
   hairline-separated rows; each row is a native <details> disclosure
   whose <summary> carries the identity + the headline £ value on one
   line (the whole line is the 44px+ touch/keyboard target -- summary is
   focusable and Enter/Space-operable natively, and browsers expose its
   expanded state; a chevron mirrors it visually), with the full
   label:value breakdown inside. Currency values are nowrap tokens with
   their own non-shrinking flex item, so a "£3,630.00" can never be
   squeezed into a vertical digit column again. ── */
.mpa-mlist { border-top: 1px solid var(--mpa-hairline); }
.mpa-mlist-item { border-bottom: 1px solid var(--mpa-hairline); }
.mpa-mlist-item > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .7rem 1rem; min-height: 44px;
}
.mpa-mlist-item > summary::-webkit-details-marker { display: none; }
.mpa-mlist-item > summary:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: -2px; border-radius: 4px; }
.mpa-mlist-title { flex: 1 1 40%; min-width: 0; font-weight: 700; font-size: var(--mpa-text-base); overflow-wrap: anywhere; }
.mpa-mlist-title a { text-decoration: none; }
.mpa-mlist-value {
  flex: 0 0 auto; margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums;
  white-space: nowrap; font-size: var(--mpa-text-base);
}
.mpa-mlist-chevron { flex: 0 0 auto; color: var(--mpa-muted); font-size: var(--mpa-text-sm); transition: transform .15s ease; }
.mpa-mlist-item[open] > summary .mpa-mlist-chevron { transform: rotate(180deg); }
.mpa-mlist-item[open] > summary { background: var(--mpa-row-hover); }
/* Optional proportional visual on its OWN full-width line under the
   title/value pair -- it never competes with the number for width. */
.mpa-mlist-bar { flex: 1 1 100%; height: 4px; border-radius: 999px; background: var(--mpa-hairline); overflow: hidden; margin-top: .15rem; }
.mpa-mlist-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--mpa-action); }
.mpa-mlist-sub { flex: 1 1 100%; font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin-top: -.15rem; overflow-wrap: anywhere; }
.mpa-mlist-body { padding: .1rem 1rem .8rem; }
.mpa-mlist-body dl { margin: 0; }
.mpa-mlist-body .mpa-mlist-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: .75rem;
  padding: .28rem 0; border-top: 1px solid var(--mpa-hairline); font-size: var(--mpa-text-md);
}
.mpa-mlist-body .mpa-mlist-row:first-child { border-top: none; }
.mpa-mlist-body dt { font-weight: 600; color: var(--mpa-muted); font-size: var(--mpa-text-xs); text-transform: uppercase; letter-spacing: .05em; flex: 0 1 auto; min-width: 0; }
.mpa-mlist-body dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; text-align: right; }
.mpa-mlist-body dd.wrap { white-space: normal; flex: 1 1 55%; overflow-wrap: anywhere; }
/* Teacher(s) as a real list on phones (§17) -- one PERSON per line, a name
   never broken letter-by-letter by a squeezed column. */
.mpa-mlist-people { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .1rem; text-align: right; }
.mpa-mlist-people a { text-decoration: none; white-space: nowrap; }

/* ── §14/§18. Labelled totals -- every figure named, never positional.
   Same label:value row grammar as the list body so the footer reads as
   the list's own sum, spanning the card's full width. ── */
.mpa-mlist-totals { padding: .6rem 1rem .8rem; background: var(--mpa-bg); border-top: 2px solid var(--mpa-line); }
.mpa-mlist-totals .mpa-mlist-totals-title { font-size: var(--mpa-text-xs); font-weight: 800; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase; color: var(--mpa-muted); margin-bottom: .25rem; }
.mpa-mlist-totals .mpa-mlist-row { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; padding: .22rem 0; font-size: var(--mpa-text-md); }
.mpa-mlist-totals dt { font-weight: 600; color: var(--mpa-ink); flex: 1 1 auto; min-width: 0; }
.mpa-mlist-totals dd { margin: 0; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; }
.mpa-mlist-totals dl { margin: 0; }

/* ── §16/§21. Ranked value list -- the phone replacement for a horizontal
   bar chart with many categories (Estimated profit by class, Staff cost
   by teacher). The TEXT ROW is the readable/touchable unit (min 44px);
   the proportional bar is decoration under it, never the target. ── */
.mpa-ranked { border-top: 1px solid var(--mpa-hairline); }
.mpa-ranked-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: .15rem .6rem;
  padding: .55rem 1rem; min-height: 44px; border-bottom: 1px solid var(--mpa-hairline);
  color: var(--mpa-ink); text-decoration: none;
}
a.mpa-ranked-row:hover { background: var(--mpa-row-hover); color: var(--mpa-ink); }
.mpa-ranked-name { flex: 1 1 40%; min-width: 0; font-weight: 600; font-size: var(--mpa-text-md); overflow-wrap: anywhere; }
.mpa-ranked-value { flex: 0 0 auto; margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; font-size: var(--mpa-text-base); }
.mpa-ranked-value.neg { color: var(--mpa-danger); }
.mpa-ranked-value.pos { color: var(--mpa-ok); }
.mpa-ranked-bar { flex: 1 1 100%; height: 4px; border-radius: 999px; background: var(--mpa-hairline); overflow: hidden; }
.mpa-ranked-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--mpa-action); min-width: 2px; }
.mpa-ranked-bar > span.neg { background: var(--mpa-danger); }
.mpa-ranked-bar > span.pos { background: var(--mpa-ok); }
.mpa-ranked-more { padding: .6rem 1rem; }

/* ── §20. Per-teacher accordion (Reports Staff Costs): the card head IS
   the toggle (same .mpa-card > details > summary pattern the Classes list
   and Class Details already use), total always visible in the head,
   chevron mirrors [open]. ── */
.mpa-card > details > summary.mpa-card-head { min-height: 44px; }
.mpa-card > details > summary.mpa-card-head:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: -2px; border-radius: var(--mpa-radius-sm); }

/* ── Family Key V1 final polish (10 Sep 2026, item A): the five Bulk
   Import "Instructions" tables (Students/Classes/Enrolments/Staff/
   Venues) share one structure -- Column | Required? | Notes -- and the
   browser's default auto table layout let Notes' long prose squeeze
   Column down until normal field names ("First Name", "Parent Email")
   wrapped unnecessarily, even with plenty of spare width on desktop.
   table-layout: fixed pins Column/Required to a width wide enough for
   every real field name across all five tables (longest is "Private
   Class Rate" / "Duration (Minutes)", ~19 chars) to stay on one line;
   Notes (no explicit width) takes 100% of whatever remains. Reverts to
   the browser's normal auto layout under Bootstrap's own xs breakpoint
   (575.98px) so a genuinely narrow screen keeps its natural wrap instead
   of three columns fighting for a fixed width that no longer fits. ── */
.mpa-import-table { table-layout: fixed; width: 100%; }
.mpa-import-table .mpa-import-col-field { width: 230px; }
.mpa-import-table .mpa-import-col-required { width: 100px; }
@media (max-width: 575.98px) {
  .mpa-import-table { table-layout: auto; }
}


/* ═══════════════════════════════════════════════════════════════════════
   USER GUIDE / HELP CENTRE (User Guide + AI Help V1; premium UX corrective
   10 Sep 2026). Additive block: every selector is .help-* (or the two
   dashboard primitives at the end) — nothing here restyles an existing
   component. Tokens only, so the tenant's accent flows through
   --mpa-tint / --mpa-ink-brand / --mpa-action exactly as on the dashboard.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Landing page ─────────────────────────────────────────────────────── */
.help-hero {
  padding: 2rem 1.5rem 1.75rem; margin-bottom: 1.5rem; text-align: center;
  background:
    radial-gradient(120% 140% at 0% 0%, var(--mpa-tint) 0%, transparent 55%),
    radial-gradient(110% 120% at 100% 100%, var(--mpa-tint) 0%, transparent 55%),
    var(--mpa-surface);
}
.help-hero-inner { max-width: 40rem; margin: 0 auto; }
.help-hero-icon {
  width: 52px; height: 52px; border-radius: 16px; display: inline-grid; place-items: center;
  background: var(--mpa-action); color: var(--mpa-on-action); font-size: 1.45rem; margin-bottom: .75rem;
}
.help-hero-title { font-size: 1.45rem; font-weight: 800; letter-spacing: -.01em; margin: 0 0 .2rem; }
.help-hero-sub { color: var(--mpa-muted); font-size: var(--mpa-text-base); margin: 0 0 1.1rem; }
.help-search { position: relative; display: flex; align-items: center; }
.help-search-icon { position: absolute; left: 1rem; color: var(--mpa-muted); pointer-events: none; }
.help-search-input {
  width: 100%; height: 3.1rem; padding: 0 2.75rem 0 2.75rem; font-size: 1rem;
  border: 1px solid var(--mpa-line); border-radius: 999px; background: var(--mpa-surface);
  box-shadow: var(--mpa-shadow); color: var(--mpa-ink); outline: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.help-search-input:focus { border-color: var(--mpa-action); box-shadow: 0 0 0 4px rgba(var(--mpa-action-rgb), .15), var(--mpa-shadow); }
.help-search-input::-webkit-search-cancel-button { display: none; }
.help-search-clear {
  position: absolute; right: .6rem; width: 2rem; height: 2rem; border: 0; border-radius: 999px;
  background: transparent; color: var(--mpa-muted); display: grid; place-items: center; cursor: pointer;
}
.help-search-clear:hover { background: var(--mpa-track); color: var(--mpa-ink); }

.help-results { margin-bottom: 1.25rem; }
.help-results-head { font-size: var(--mpa-text-sm); font-weight: 700; color: var(--mpa-muted); text-transform: uppercase; letter-spacing: var(--mpa-tracking-label); margin: 0 0 .5rem .25rem; }
.help-results-list { display: grid; gap: .5rem; }
.help-result {
  display: grid; gap: .15rem; padding: .8rem 1rem; text-decoration: none; color: var(--mpa-ink);
  background: var(--mpa-surface); border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius-sm);
  transition: border-color .15s ease, transform .15s ease;
}
.help-result:hover { border-color: var(--mpa-action); color: var(--mpa-ink); transform: translateY(-1px); }
.help-result-crumb { font-size: var(--mpa-text-xs); font-weight: 700; color: var(--mpa-ink-brand); text-transform: uppercase; letter-spacing: .06em; }
.help-result-title { font-weight: 700; font-size: var(--mpa-text-base); }
.help-result-excerpt { font-size: var(--mpa-text-sm); color: var(--mpa-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.help-results-msg { padding: .75rem 1rem; border-radius: var(--mpa-radius-sm); background: var(--mpa-surface); border: 1px solid var(--mpa-line); color: var(--mpa-muted); font-size: var(--mpa-text-md); }
.help-results-msg.warn { background: var(--mpa-warn-bg); border-color: transparent; color: var(--mpa-warn-ink); }

.help-tasks { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .5rem; }
/* Mobile Popular Tasks (final micro-corrective, 11 Sep 2026): below
   768px .mpa-action-btn is inline-flex sized to its own label, so
   flex-wrap put one chip per row (10 rows for 10 tasks) — every task
   name was wider than half a phone screen at normal size. A real grid
   forces two even columns instead (~5 rows), each chip stretched to
   fill its cell; padding/gap/font trimmed slightly (never the label
   itself hidden or truncated) and the label allowed to wrap onto a
   second line rather than overflow. Very narrow phones (<360px) fall
   back to one column so nothing gets uncomfortably cramped. Desktop/
   tablet (>=768px) keeps the original flex-wrap chip row untouched. */
@media (max-width: 767.98px) {
  .help-tasks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .45rem; }
  .help-tasks .mpa-action-btn {
    width: 100%; padding: .4rem .5rem .4rem .4rem; gap: .4rem;
    font-size: var(--mpa-text-sm); white-space: normal; overflow-wrap: anywhere; line-height: 1.2;
  }
  .help-tasks .mpa-action-btn .mpa-icon-tile.sm { flex-shrink: 0; }
}
@media (max-width: 359.98px) {
  .help-tasks { grid-template-columns: 1fr; }
}
.help-cat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: 1rem; }
.help-cat-card { display: flex; flex-direction: column; }
.help-cat-head { display: flex; align-items: flex-start; gap: .8rem; padding: 1rem 1.1rem .5rem; }
.help-cat-title { font-size: 1rem; font-weight: 800; margin: .15rem 0 .1rem; letter-spacing: -.01em; }
.help-cat-blurb { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin: 0; }
.help-cat-list { list-style: none; margin: 0; padding: .25rem .5rem .6rem; }
.help-cat-link {
  display: grid; gap: .05rem; padding: .5rem .6rem; border-radius: var(--mpa-radius-sm);
  text-decoration: none; color: var(--mpa-ink); transition: background .12s ease;
}
.help-cat-link:hover { background: var(--mpa-tint); color: var(--mpa-ink); }
.help-cat-link-title { font-weight: 700; font-size: var(--mpa-text-base); }
.help-cat-link-summary { font-size: var(--mpa-text-sm); color: var(--mpa-muted); }

/* ── Chapter page: layout ─────────────────────────────────────────────── */
.help-chapter-heading { min-width: 0; }
.help-crumb { font-size: var(--mpa-text-sm); color: var(--mpa-muted); display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-bottom: .15rem; }
.help-crumb a { color: var(--mpa-ink-brand); text-decoration: none; font-weight: 600; }
.help-crumb a:hover { text-decoration: underline; }
.help-crumb-sep { opacity: .6; }

/* Help drawer title link (final visual polish, 11 Sep 2026, Part A):
   same brand-coloured-text-link convention as .help-crumb a above — the
   school's own link colour token, not a new one — with normal focus-
   visible affordance (this app has no existing global <a>:focus-visible
   ring to inherit, so one is added here, matching the same outline
   treatment .ai-cite-link already uses for the same reason). Identical
   in the desktop panel and the mobile 100vw drawer — one element, no
   second implementation. */
.help-drawer-title { color: var(--mpa-ink-brand); text-decoration: none; display: inline-flex; align-items: center; gap: .4rem; }
.help-drawer-title:hover { color: var(--mpa-ink-brand); text-decoration: underline; }
.help-drawer-title:focus-visible { outline: 2px solid rgba(var(--mpa-action-rgb), .35); outline-offset: 2px; border-radius: 4px; }

/* Final corrective (11 Sep 2026): the grid MUST stretch its side columns
   to the full row height (the default; the previous `align-items:start`
   shrank the nav/TOC columns to their own content height, which is why
   `position:sticky` inside them had no room to stick — the TOC never
   stayed visible and the collapsed "CHAPTERS" rail floated mid-page).
   Each side column is a full-height grid item; the STICKY thing is a
   child inside it (.help-nav-sticky / .help-toc-sticky / .help-nav-expand),
   pinned near the top of the viewport while that column has room. The
   middle column is fluid; prose inside it is capped by measure (below),
   so tables and figures can use the whole column while paragraphs stay
   readable. */
.help-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.5rem; align-items: stretch; position: relative; }
@media (min-width: 992px) {
  .help-layout { grid-template-columns: 16rem minmax(0, 1fr); column-gap: 2rem; }
  .help-layout.nav-collapsed { grid-template-columns: 2.75rem minmax(0, 1fr); column-gap: 1.25rem; }
}
@media (min-width: 1200px) {
  .help-layout { grid-template-columns: 16rem minmax(0, 1fr) 13rem; }
  .help-layout.nav-collapsed { grid-template-columns: 2.75rem minmax(0, 1fr) 13rem; }
}
@media (min-width: 1600px) {
  .help-layout { grid-template-columns: 17rem minmax(0, 1fr) 14rem; column-gap: 2.5rem; }
  .help-layout.nav-collapsed { grid-template-columns: 2.75rem minmax(0, 1fr) 14rem; }
}

/* Left nav: a normal column at >=lg (Bootstrap's .offcanvas-lg turns the
   SAME element into a real drawer below that). The pane's own header —
   "Chapters" + a collapse button — sits at the TOP of the pane, like a
   Word Navigation pane; collapsing hides the pane and releases its width
   back to the article, leaving only a slim rail with an expand button at
   the top (never a vertical tab down the side). */
@media (min-width: 992px) {
  .help-nav { min-width: 0; }
  .help-nav-sticky { position: sticky; top: 1rem; max-height: calc(100vh - 2rem); overflow: auto; overscroll-behavior: contain; }
  .help-layout.nav-collapsed .help-nav { display: none; }
}
@media (max-width: 991.98px) {
  .help-nav-sticky { position: static; max-height: none; overflow: visible; }
}
.help-nav-body { padding: 0; }
.help-nav-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .15rem .3rem .5rem .6rem; margin-bottom: .25rem; border-bottom: 1px solid var(--mpa-hairline); }
.help-nav-top-label { font-size: var(--mpa-text-xs); font-weight: 800; letter-spacing: .11em; text-transform: uppercase; color: var(--mpa-muted); }
.help-nav-collapse, .help-nav-expand {
  border: 1px solid var(--mpa-line); background: var(--mpa-surface); color: var(--mpa-muted);
  border-radius: var(--mpa-radius-sm); cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; padding: 0; font-size: var(--mpa-text-base); flex-shrink: 0;
}
.help-nav-collapse:hover, .help-nav-expand:hover, .help-nav-collapse:focus-visible, .help-nav-expand:focus-visible {
  color: var(--mpa-ink-brand); border-color: var(--mpa-action); background: var(--mpa-tint);
}
.help-nav-rail { display: flex; justify-content: center; min-width: 0; }
.help-nav-expand { position: sticky; top: 1rem; align-self: start; width: 2.25rem; height: 2.25rem; }
.help-nav-group { margin-bottom: .75rem; }
.help-nav-group-label { display: flex; align-items: center; gap: .4rem; font-size: var(--mpa-text-xs); font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--mpa-muted); padding: .25rem .6rem; }
.help-nav-group-label i { color: var(--mpa-ink-brand); font-size: var(--mpa-text-md); }
.help-nav-link {
  display: block; padding: .38rem .6rem .38rem .85rem; margin: 1px 0; border-radius: var(--mpa-radius-sm);
  font-size: var(--mpa-text-md); line-height: 1.3; color: var(--mpa-ink); text-decoration: none; border-left: 3px solid transparent;
  overflow-wrap: anywhere;   /* long chapter titles wrap, never clip */
}
.help-nav-link:hover { background: var(--mpa-tint); color: var(--mpa-ink); }
.help-nav-link.active { background: var(--mpa-tint); color: var(--mpa-ink-brand); font-weight: 700; border-left-color: var(--mpa-action); }
/* Keyboard focus (final visual micro-fix, 11 Sep 2026, Part A — added
   when this class was reused for the drawer's own chapter list, which
   needed one; benefits the full Help Centre's left nav identically,
   same class, no second rule). Same outline treatment already used for
   .ai-cite-link/.help-drawer-title, not a new one. */
.help-nav-link:focus-visible { outline: 2px solid rgba(var(--mpa-action-rgb), .35); outline-offset: 1px; }
@media (max-width: 991.98px) {
  .help-nav.offcanvas-lg { --bs-offcanvas-width: min(20rem, 90vw); }
  .help-nav .offcanvas-body { padding: .75rem; }
}

/* ── Chapter page: article typography & rhythm ────────────────────────── */
/* Final corrective (11 Sep 2026): the article COLUMN is fluid (it takes
   whatever the grid gives it, which grows on a large monitor); PROSE
   inside it is capped to a comfortable measure, while tables and figures
   (direct children NOT in the capped list) may use the full column.
   Rhythm tightened a notch from the .2 pass (paragraph .65->.55rem,
   section 1.6->1.35rem, line-height 1.6->1.55): still airy, headings
   keep clear separation, never dense documentation.

   MICRO-CORRECTIVE (11 Sep 2026, Rory's Surface Pro finding): 72ch/78ch
   left visibly unused space between the article and the "In this
   chapter" rail. Root cause was leaving too much margin, not the ch
   unit itself — this app runs a 13px desktop root (app.css's own
   "Desktop density scale", >=992px), so 72ch at .95rem was only
   ~470-490px wide against a fluid column that measures 700-1080px
   across the real desktop breakpoints (992-1199: ~700-740px with no
   third column yet; 1200-1599: ~720-900px; >=1600: ~1000px+, computed
   from .main-content's 2rem padding, the nav/TOC column widths and
   gaps above — verified arithmetically, no live server was available
   this pass to measure in-browser). 85ch/88ch (~570-600px) comfortably
   fits every one of those, with headroom left for callouts/padding, so
   the column never overflows while using materially more of a Surface
   Pro or larger screen. Scoped to >=992px only (where the multi-column
   desktop grid actually exists) — below that, a single-column tablet
   layout could otherwise stretch prose close to full width on a
   portrait iPad; mobile/tablet keep their unchanged, narrower measure. */
.help-article { min-width: 0; font-size: var(--mpa-text-base); line-height: 1.55; color: var(--mpa-ink); }
.help-lead, .help-section > p, .help-section > ul, .help-section > ol, .help-section > blockquote,
.help-section > h2, .help-section > h3, .help-section > h4, .help-toc-mobile, .help-pager { max-width: 72ch; }
@media (min-width: 992px) {
  .help-lead, .help-section > p, .help-section > ul, .help-section > ol, .help-section > blockquote,
  .help-section > h2, .help-section > h3, .help-section > h4, .help-toc-mobile, .help-pager { max-width: 85ch; }
}
@media (min-width: 1600px) {
  .help-lead, .help-section > p, .help-section > ul, .help-section > ol, .help-section > blockquote,
  .help-section > h2, .help-section > h3, .help-section > h4, .help-toc-mobile, .help-pager { max-width: 88ch; }
}
.help-lead { font-size: var(--mpa-text-lg); line-height: 1.5; color: var(--mpa-ink); margin-bottom: 1.15rem; }
.help-lead p { margin: 0; }
.help-section { padding-top: .2rem; margin-bottom: 1.35rem; scroll-margin-top: 1.25rem; border-radius: var(--mpa-radius-sm); }
.help-section::after { content: ""; display: block; clear: both; }   /* contains a floated side figure */
.help-h2 { font-size: 1.22rem; font-weight: 800; letter-spacing: -.01em; margin: 0 0 .5rem; padding-bottom: .3rem; border-bottom: 1px solid var(--mpa-hairline); }
.help-article h3 { font-size: 1rem; font-weight: 700; margin: .9rem 0 .3rem; }
.help-article p { margin: 0 0 .55rem; }
.help-article ul, .help-article ol { margin: 0 0 .65rem; padding-left: 1.35rem; }
.help-article li { margin-bottom: .22rem; }
.help-article li > p { margin-bottom: .15rem; }
.help-article ol > li::marker { font-weight: 700; color: var(--mpa-ink-brand); }
.help-article strong { font-weight: 700; }
.help-article code { font-size: .86em; background: var(--mpa-track); padding: .05em .35em; border-radius: 4px; color: var(--mpa-ink); }
.help-article a { color: var(--mpa-ink-brand); }
.help-article > .help-section:last-of-type { margin-bottom: .75rem; }

.help-callout {
  margin: .5rem 0 .8rem; padding: .6rem .9rem .6rem 1rem; border-left: 4px solid var(--mpa-info);
  background: var(--mpa-info-bg); border-radius: 0 var(--mpa-radius-sm) var(--mpa-radius-sm) 0; font-size: var(--mpa-text-base);
}
.help-callout p { margin: 0; }
.help-callout p + p { margin-top: .4rem; }
.help-callout strong:first-child { color: var(--mpa-info-ink); }
.help-callout-tip { border-left-color: var(--mpa-ok); background: var(--mpa-ok-bg); }
.help-callout-tip strong:first-child { color: var(--mpa-ok-ink); }
.help-callout-warn { border-left-color: var(--mpa-warn); background: var(--mpa-warn-bg); }
.help-callout-warn strong:first-child { color: var(--mpa-warn-ink); }

/* Guide tables (final corrective, 11 Sep 2026): the header row is STICKY
   while the table is in view, on desktop and mobile, so a long "status /
   meaning / parent can see it?" table never loses its column names. The
   wrapper is deliberately NOT a scroll container by default — a scroll
   container traps a sticky header inside itself, which is exactly why
   the .2 pass's `overflow-x:auto` wrapper could never have a sticky
   header. It becomes one (.is-scrollable, set by mpaHelpFitTables in
   base.html) only when the table is genuinely wider than its column,
   where horizontal scrolling beats crushing columns unreadably. Sticky
   stops naturally when the table's own end scrolls past. Guide-only
   (.help-table) — a wider app-table primitive is a logged P2, not this
   pass. border-collapse:separate because collapsed borders don't travel
   with a sticky <th>; the inset shadow is the header's bottom rule.

   MICRO-CORRECTIVE (11 Sep 2026, table sizing): root cause of every
   guide table stretching to the full (now-wider) article column
   regardless of content — `.help-table` forced `width:100%` and the
   wrapper `<div>` is a plain block element, which always fills its
   parent's width by default whatever its child does. `width:
   fit-content` makes the WRAPPER shrink-to-fit its table's own
   intrinsic/preferred width (a short 3-column status table now sizes to
   its content, not the column); `max-width:100%` is the ceiling so a
   genuinely wide table still grows up to the article width and never
   overflows the page. The table itself drops its forced 100% for the
   browser's normal auto-layout (columns sized to content, same cell
   padding below — never crushed to single words). mpaHelpFitTables'
   `table.scrollWidth > wrap.clientWidth` check is unaffected: a
   naturally-fitting table's wrapper now equals the table's own width
   (never triggers .is-scrollable); a table whose intrinsic width
   exceeds the column has its fit-content wrapper clamped at max-width
   (real overflow, correctly triggers it). Sticky header/opaque
   background/z-index — all above — are untouched by this change. */
.help-table-wrap { overflow: visible; width: fit-content; max-width: 100%; margin: .25rem 0 .9rem; border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius-sm); }
.help-table-wrap.is-scrollable { overflow-x: auto; }
.help-table { width: auto; margin: 0; border-collapse: separate; border-spacing: 0; font-size: var(--mpa-text-md); }
.help-table th, .help-table td { padding: .5rem .7rem; text-align: left; vertical-align: top; border-top: 1px solid var(--mpa-hairline); }
.help-table thead th {
  position: sticky; top: 0; z-index: 2; border-top: 0; background: var(--mpa-bg);
  box-shadow: inset 0 -1px 0 var(--mpa-line);
  font-size: var(--mpa-text-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--mpa-muted);
}
.help-table thead th:first-child { border-top-left-radius: var(--mpa-radius-sm); }
.help-table thead th:last-child { border-top-right-radius: var(--mpa-radius-sm); }
.help-table tbody tr:first-child td { border-top: 0; }

/* Figures / screenshots (optional; page reads fine with none) */
.help-figure { margin: .5rem 0 1rem; max-width: 100%; }
.help-figure-link { display: block; border-radius: var(--mpa-radius-sm); overflow: hidden; border: 1px solid var(--mpa-line); box-shadow: var(--mpa-shadow); background: var(--mpa-surface); }
.help-figure img { display: block; width: 100%; height: auto; max-width: 100%; }
/* Side figure (final corrective, 11 Sep 2026): an author may mark a
   screenshot `{: .side }` (attr_list) so on a wide desktop it sits beside
   its short explanatory block instead of pushing it down; below 1200px
   it stacks full-width like any other figure, never causing horizontal
   scroll. .help-section::after clears the float. */
@media (min-width: 1200px) {
  .help-figure-side { float: right; width: min(46%, 32rem); margin: .15rem 0 .75rem 1.5rem; }
}
.help-figure figcaption { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin-top: .4rem; padding: 0 .2rem; }
.help-lightbox-img { max-width: 100%; max-height: 82vh; display: block; margin: 0 auto; border-radius: var(--mpa-radius-sm); }

/* Section highlight after a TOC / deep-link click: quiet, ~2s, no motion */
@keyframes help-flash {
  0%   { background-color: rgba(var(--mpa-action-rgb), .12); box-shadow: inset 4px 0 0 var(--mpa-action); }
  60%  { background-color: rgba(var(--mpa-action-rgb), .08); box-shadow: inset 4px 0 0 var(--mpa-action); }
  100% { background-color: transparent; box-shadow: inset 4px 0 0 transparent; }
}
.help-section.help-flash { animation: help-flash 2s ease-out; padding-left: .5rem; margin-left: -.5rem; }

/* "In this chapter": sticky scroll-spied list (>=xl) / <details> below.
   The aside is a full-height grid item (see .help-layout above), so this
   inner block genuinely stays pinned near the top-right while the reader
   scrolls the chapter, and scrolls internally if a chapter has more
   sections than fit. There is no fixed app chrome on desktop, so a small
   top offset is enough; sections keep scroll-margin-top so a jumped-to
   heading is never tucked under anything. */
.help-toc { min-width: 0; }
.help-toc-sticky { position: sticky; top: 1.25rem; max-height: calc(100vh - 2.5rem); overflow: auto; overscroll-behavior: contain; }
.help-toc-label { font-size: var(--mpa-text-xs); font-weight: 800; letter-spacing: .11em; text-transform: uppercase; color: var(--mpa-muted); margin-bottom: .45rem; }
.help-toc-list { list-style: none; margin: 0; padding: 0; border-left: 2px solid var(--mpa-line); }
.help-toc-list li { margin: 0; }
.help-toc-list a {
  display: block; padding: .28rem .7rem; margin-left: -2px; border-left: 2px solid transparent;
  font-size: var(--mpa-text-sm); color: var(--mpa-muted); text-decoration: none; line-height: 1.35;
}
.help-toc-list a:hover { color: var(--mpa-ink); }
.help-toc-list a.active { color: var(--mpa-ink-brand); font-weight: 700; border-left-color: var(--mpa-action); }
.help-toc-mobile { margin: 0 0 1.1rem; border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius-sm); background: var(--mpa-surface); }
.help-toc-mobile summary { cursor: pointer; padding: .55rem .85rem; font-size: var(--mpa-text-sm); font-weight: 800; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase; color: var(--mpa-muted); }
.help-toc-mobile-list { list-style: none; margin: 0; padding: 0 .85rem .6rem; }
.help-toc-mobile-list a { display: block; padding: .3rem 0; font-size: var(--mpa-text-md); color: var(--mpa-ink-brand); text-decoration: none; }

.help-pager { display: flex; justify-content: space-between; gap: 1rem; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--mpa-hairline); }
.help-pager-link { display: grid; gap: .1rem; text-decoration: none; color: var(--mpa-ink); max-width: 48%; }
.help-pager-link.next { text-align: right; margin-left: auto; }
.help-pager-label { font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--mpa-muted); }
.help-pager-title { font-weight: 700; color: var(--mpa-ink-brand); }
.help-pager-link:hover .help-pager-title { text-decoration: underline; }

@media (max-width: 991.98px) {
  .help-section { scroll-margin-top: 4.5rem; }
  .help-hero { padding: 1.5rem 1rem 1.25rem; }
  .help-cat-grid { grid-template-columns: 1fr; }
}

/* ── Dashboard header row primitive (Rory, 10 Sep 2026) ───────────────────
   Generalises the admin-only .mpa-dashboard-attention-row so EVERY role's
   dashboard can put its first section label on one row with its
   right-hand date text (admin/teacher: the current term's date range;
   reception: today's date). The old class stays as an alias so nothing
   that targets it breaks. */
.mpa-dashboard-section-row, .mpa-dashboard-attention-row { justify-content: space-between; flex-wrap: wrap; row-gap: .3rem; }
.mpa-dashboard-section-label, .mpa-dashboard-attention-label { display: flex; align-items: center; gap: .55rem; }
.mpa-dashboard-section-date, .mpa-dashboard-term-range { margin: 0; }

/* Ask AI guide citations (Part F, 10 Sep 2026): a guide source pill is a real link into /help; data-source pills stay plain text. */
.ai-cite-link { font-size: var(--mpa-text-xs); font-weight: 600; background: var(--mpa-tint); color: var(--mpa-ink-brand); text-decoration: none; display: inline-flex; align-items: center; gap: .25rem; }

/* ── Door scan-result overlay (13 Sep 2026 mobile-scan-feedback corrective) ──
   Rory's real PROD phone acceptance: the verdict rendered correctly but
   sat in normal document flow below the counters + camera, so on a short
   mobile viewport it was out of view and looked like the scan had failed.
   Fixed to the viewport (not the scanner container) so it is guaranteed
   visible the instant a decision lands, with zero scroll, regardless of
   where the page happens to be scrolled to (manual-search flows can also
   route into #door-result via the same submitScan() path). Sits above the
   sticky page header (1020) and the note-backdrop (1050); stays below the
   PWA install banner (9999), which is rare enough not to matter here. Same
   semantic tokens as .mpa-alert — no new colour language. */
.door-scan-result {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 1085;
  padding: max(.6rem, env(safe-area-inset-top)) .6rem 0;
  display: flex; justify-content: center;
  pointer-events: none; /* only the box itself needs clicks */
}
.door-scan-result-box {
  pointer-events: auto;
  width: 100%; max-width: 480px;
  border-radius: 0 0 var(--mpa-radius) var(--mpa-radius);
  padding: 1rem 1.25rem 1.15rem;
  text-align: center;
  box-shadow: var(--mpa-shadow-lift);
  animation: door-scan-result-in .18s ease-out;
}
.door-scan-result-box.ok     { background: var(--mpa-ok-bg);     color: var(--mpa-ok-ink); }
.door-scan-result-box.warn   { background: var(--mpa-warn-bg);   color: var(--mpa-warn-ink); }
.door-scan-result-box.danger { background: var(--mpa-danger-bg); color: var(--mpa-danger-ink); }
@keyframes door-scan-result-in {
  from { transform: translateY(-100%); opacity: 0; }
  to   { transform: translateY(0);     opacity: 1; }
}
.door-scan-result-icon { font-size: 2.75rem; line-height: 1; }
.door-scan-result-title { font-size: 1.5rem; font-weight: 800; text-transform: uppercase; letter-spacing: .02em; margin-top: .1rem; }
.door-scan-result-lines { font-size: var(--mpa-text-lg); margin-top: .15rem; }
@media (max-width: 420px) {
  .door-scan-result-icon { font-size: 2.25rem; }
  .door-scan-result-title { font-size: 1.25rem; }
  .door-scan-result-lines { font-size: var(--mpa-text-base); }
}
@media (prefers-reduced-motion: reduce) {
  .door-scan-result-box { animation: none; }
}
.ai-cite-link:hover, .ai-cite-link:focus-visible { background: var(--mpa-tint-strong); color: var(--mpa-ink-brand); text-decoration: underline; outline: 2px solid rgba(var(--mpa-action-rgb), .35); outline-offset: 1px; }


/* ── Parent class drawer rhythm (Rory's acceptance, 15 Sep 2026) ──────────
   .mpa-cd-touch keeps the 44px tap target below lg (unchanged mobile feel);
   on desktop the "Withdraw interest" row collapses to the ordinary small-
   button height so a status note no longer reads as an oversized banner.
   .mpa-cd-interest: desktop breathing room above "Interested in this
   class?" is one step (8px + 8px over the rule), mobile keeps a touch more. */
.mpa-cd-touch { min-height: 44px; }
@media (min-width: 992px) {
  .mpa-cd-touch { min-height: auto; }
}
@media (max-width: 991.98px) {
  .mpa-cd-interest { padding-top: .75rem !important; margin-top: .75rem !important; }
}

/* ── Timetable drawer alert rhythm (Rory's acceptance, 15 Sep 2026) ───────
   The enrol/remove success or refusal banner now swaps into #panel-alerts
   in place (no whole-drawer re-render); give it the same .5rem gap above
   as below so the Students section keeps one rhythm before and after. */
.mpa-panel-alert { margin-top: .5rem; }


/* ── Solis Support AI: the per-answer feedback row (AI-SUPPORT-V1-A) ─────
   Sits under an assistant answer inside the chat panel. Deliberately
   quiet: two small outline chips and a text link, nothing that competes
   with the answer itself or looks like a primary action. The chips wear
   the panel's own accent through --ai-assistant-accent, so they are green
   on the support plane by inheritance and would follow any future plane
   without a new rule. */
.support-ai-feedback { min-height: 1.75rem; }
.support-ai-rate {
  font-size: var(--mpa-text-xs);
  line-height: 1;
  padding: .28rem .6rem;
  border-radius: 999px;
  border: 1px solid var(--ai-assistant-accent);
  background: transparent;
  color: var(--ai-assistant-accent);
  font-weight: 600;
}
.support-ai-rate:hover,
.support-ai-rate:focus-visible {
  background: var(--ai-assistant-accent);
  color: #fff;
}
.support-ai-escalate { color: var(--ai-assistant-accent) !important; font-weight: 600; }
.support-ai-escalate:hover, .support-ai-escalate:focus-visible { text-decoration: underline !important; }

/* The Support page's own "Ask Support AI" card (templates/support.html).
   It opens the SAME panel base.html renders — there is no second chat
   implementation — so this is a button, not a widget. */
.support-ai-cta {
  background: var(--support-ai-accent);
  border-color: var(--support-ai-accent);
  color: #fff;
  font-weight: 600;
}
.support-ai-cta:hover, .support-ai-cta:focus-visible {
  background: #0a6a4b;
  border-color: #0a6a4b;
  color: #fff;
}
.support-ai-icon {
  width: 2.25rem; height: 2.25rem; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(11, 124, 87, .12);
  color: var(--support-ai-accent);
  flex-shrink: 0;
}

/* AI-SUPPORT-V1-A: the green Setup-help launcher's bottom scroll gutter.
   See base.html's own note — this exists for the ONE non-transient
   collision a floating control can cause: a page whose last control sits
   at the very end of the document, with nothing left to scroll. 5.5rem
   clears the launcher's 1.5rem offset plus its own height at every
   breakpoint (desktop pill ~44px, mobile FAB 48px). Scoped to the support
   plane only, so no page the orange launcher already ships on shifts. */
body.has-support-ai-launcher main { padding-bottom: 5.5rem; }

/* AI-SUPPORT-V1-A final corrective §A: the Support form has just been
   prefilled from a Support AI answer the person said didn't help. A brief
   ring draws the eye to it -- the panel has closed and the page scrolled,
   and without this the form looks identical to the empty one they walked
   past on the way in. Animation, not a permanent state: it says "this
   changed", not "this is special". */
.support-form-prefilled { animation: supportPrefillFlash 1.6s ease-out 1; border-radius: var(--mpa-radius-sm); }
@keyframes supportPrefillFlash {
  0%   { box-shadow: 0 0 0 3px rgba(11, 124, 87, .35); }
  100% { box-shadow: 0 0 0 3px rgba(11, 124, 87, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .support-form-prefilled { animation: none; }
}

/* AI-SUPPORT-V1-A §C: Support AI's deterministic "Go to" chips. Reads as a
   row of small destination buttons under an answer -- clearly actions, and
   clearly distinct from the User-Guide citation pills above them
   (.ai-cite-link, which are sources, not destinations). Wears the panel's
   own accent through --ai-assistant-accent, like the rest of the widget. */
.support-ai-links { align-items: center; }
.support-ai-link {
  display: inline-flex; align-items: center; gap: .25rem;
  font-size: var(--mpa-text-xs); font-weight: 600; line-height: 1;
  padding: .3rem .6rem; border-radius: 999px;
  border: 1px solid var(--ai-assistant-accent);
  color: var(--ai-assistant-accent); background: transparent;
  text-decoration: none;
}
.support-ai-link::before { content: "\2192"; opacity: .7; }
.support-ai-link:hover, .support-ai-link:focus-visible {
  background: var(--ai-assistant-accent); color: #fff; text-decoration: none;
}

/* ============================================================
   ADMIN CONSOLE — shared components (UI-CONSISTENCY-1, 21 Sep 2026)
   ============================================================
   Before this pass the Admin Console was a second style island: seven
   platform templates carried ~575 lines of page-local <style> defining
   196 selectors under five invented prefixes, 62 of them duplicated
   across files and 14 silently diverged (the audit's §12). AI-SUPPORT-
   V1-A had already pulled the .aiu-* family into ONE Jinja macro
   (partials/_ai_console.html); this section moves that macro's
   stylesheet, the tooltip runtime's styles (partials/_aiu_tips.html) and
   the .wt-* / .aiu-fam-* rules the Watchtower pages each re-declared
   into the ONE stylesheet every page already loads. The Console AI /
   Tenant AI / Support AI pages and the Support queue now inherit these
   the same way Students inherits .mpa-table: by class, from app.css,
   with nothing to call and nothing to copy.

   The rules below are the accepted implementations, moved verbatim (only
   literal font sizes were re-pointed at the --mpa-text-* scale, which is
   value-identical on the reference pages). Per-page column templates
   (grid-template-columns) stay in their templates. */

  /* ── tab header + period switch ─────────────────────────────────────── */
  .aiu-tabs, .aiu-periods { display: inline-flex; gap: .15rem; background: var(--mpa-hairline); border-radius: 999px; padding: .2rem; margin-bottom: 1rem; flex-wrap: wrap; }
  .aiu-period-btn { border: none; background: transparent; padding: .35rem 1rem; border-radius: 999px; font-size: var(--mpa-text-sm); font-weight: 600;
                    color: var(--mpa-muted); cursor: pointer; transition: background .12s, color .12s, box-shadow .12s;
                    text-decoration: none; display: inline-block; }
  .aiu-period-btn:hover { color: var(--mpa-ink); }
  .aiu-period-btn.active { background: var(--mpa-surface); color: var(--mpa-ink-brand); box-shadow: var(--mpa-shadow); }
  .aiu-period-btn:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 1px; }

  /* ── zero-data state ──────────────────────────────────────────────── */
  .aiu-empty { text-align: center; padding: 2.75rem 1.5rem; background: var(--mpa-surface); border: 1px dashed var(--mpa-line);
               border-radius: var(--mpa-radius); margin-bottom: 1.25rem; }
  .aiu-empty.aiu-empty-flush { border: none; border-radius: 0; margin-bottom: 0; }
  .aiu-empty i { font-size: 1.7rem; color: var(--mpa-ink-brand); opacity: .55; }
  .aiu-empty h3 { font-size: var(--mpa-text-base); font-weight: 700; color: var(--mpa-ink); margin: .6rem 0 .3rem; }
  .aiu-empty p { font-size: var(--mpa-text-md); color: var(--mpa-muted); margin: 0 auto; max-width: 40ch; }

  /* ── KPI cards (14 Sep 2026: centred, consistent, CARD-as-hover-target) ── */
  .aiu-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(155px, 1fr)); gap: .7rem; margin-bottom: 1.25rem; }
  /* justify-content:center + a min-height keeps every card the same height
     AND vertically centres its content, so a card with a sub-line and a
     card without still read as the same object. */
  .aiu-kpi { background: var(--mpa-surface); border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius); box-shadow: var(--mpa-shadow);
             padding: .95rem 1rem; min-height: 5.6rem; min-width: 0; display: flex; flex-direction: column;
             align-items: center; justify-content: center; text-align: center; gap: .25rem;
             transition: border-color .12s, box-shadow .12s; }
  /* The card IS the tooltip trigger on pointer devices (the wiring script
     adds this class and tabindex at runtime). */
  /* The filter-card variant (§B1): identical geometry to a plain card --
     only the affordances differ, so a row of five reads as one set. */
  .aiu-kpi-link { text-decoration: none; color: inherit; }
  .aiu-kpi-link:hover { border-color: var(--mpa-ink-brand); color: inherit; box-shadow: var(--mpa-shadow-lift); }
  .aiu-kpi-link:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }
  .aiu-kpi-link.active { border-color: var(--mpa-ink-brand); box-shadow: 0 0 0 2px var(--mpa-tint); }
  .aiu-kpi-link.active .aiu-kpi-value { color: var(--mpa-ink-brand); }
  /* A link is already a pointer target; `help` would lie about what
     clicking does. The tooltip still fires on hover and focus. */
  .aiu-kpi.aiu-tip-target { cursor: help; }
  a.aiu-kpi.aiu-tip-target { cursor: pointer; }
  .aiu-kpi.aiu-tip-target:hover { border-color: var(--mpa-ink-brand); box-shadow: 0 0 0 2px var(--mpa-tint); }
  .aiu-kpi.aiu-tip-target:focus-visible { outline: none; border-color: var(--mpa-ink-brand); box-shadow: 0 0 0 2px var(--mpa-tint); }
  /* .68rem, deliberately NOT the --mpa-text-xs token: the Console's 7- and
     8-up KPI rows at 1440px are sized to the line, and the token's .7rem
     wraps "Solis-paid support spend" / "Schools using Support AI" onto
     two lines on Support AI alone, breaking the accepted equal-height
     Tokens card across the three AI pages (pw_ai_support_v1a R17). */
  .aiu-kpi-label { display: flex; align-items: center; justify-content: center; gap: .3rem; font-size: .68rem; font-weight: 700;
                   letter-spacing: .05em; text-transform: uppercase; color: var(--mpa-muted); }
  /* NOTE: the (i) glyph's own CSS lives in partials/_aiu_tips.html, which
     is THE tooltip implementation for the whole app (§B3). It is not
     repeated here -- two copies of a capability query are two chances for
     them to disagree, which is exactly how desktop kept showing icons. */
  /* white-space: nowrap -- "value + unit" is always ONE visual reading and
     must never break across two lines. */
  .aiu-kpi-value { font-size: 1.85rem; font-weight: 800; letter-spacing: -.025em; line-height: 1.1; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .aiu-kpi-sub { font-size: var(--mpa-text-xs); color: var(--mpa-muted); font-variant-numeric: tabular-nums; line-height: 1.2; }
  .aiu-kpi-pair { display: flex; justify-content: center; gap: 1.4rem; margin-top: .05rem; }
  .aiu-kpi-pair > div { display: flex; flex-direction: column; align-items: center; }
  .aiu-kpi-pair-value { display: block; font-size: var(--mpa-text-xl); font-weight: 800; letter-spacing: -.025em; line-height: 1.1; font-variant-numeric: tabular-nums; }
  /* .66rem, not the token, for the same reason as .aiu-kpi-label above: the
     Tokens 2x2 sits in an 8-up row on Support AI and "CACHE WRITE" wraps at
     .7rem, growing that one Tokens card past its two siblings. */
  .aiu-kpi-pair-label { font-size: .66rem; color: var(--mpa-muted); text-transform: uppercase; letter-spacing: .04em; margin-top: .05rem; }
  /* Tokens card 2x2 (corrective 3): two EQUAL COLUMN TRACKS, each cell a
     centred value-over-label stack, second row a size step down. This is
     what makes the four figures share two column centres at every width. */
  /* Final micro-polish (21 Sep 2026): row-gap .5rem -> .3rem. The
     typography of this card is identical on all three AI pages (one
     macro, one stylesheet -- measured by qa/pw_ai_support_v1a.py R17);
     what made it read as heavier was its HEIGHT against a plain KPI. A
     tighter row gap is the only change, here, once. */
  .aiu-kpi-grid2 { display: grid; grid-template-columns: 1fr 1fr; column-gap: 1.2rem; row-gap: .3rem; margin-top: .05rem; width: 100%; max-width: 20rem; }
  .aiu-kpi-cell { display: flex; flex-direction: column; align-items: center; min-width: 0; }
  .aiu-kpi-cache-value { display: block; font-size: var(--mpa-text-lg); font-weight: 800; letter-spacing: -.02em; line-height: 1.1; font-variant-numeric: tabular-nums; }

  /* ── recent requests: row list (same language as .wt-row) ──────────── */
  /* NOTE: the column TEMPLATE lives on each page's own *-req-cols class --
     the three lists have different columns. Everything else is shared. */
  /* ── §E/§N (21 Sep 2026): ONE DATA SURFACE, whether the list is a
     <table> or a CSS grid. Every value below is lifted from
     `.mpa-table` in app.css so that Admin Console AI, Tenant AI,
     Support AI, the Support queue and /students read as the same
     component: white rows, hairline rules, the same type size, the
     same row padding, the same hover tint, the same uppercase heading.
     If .mpa-table's tokens change, change these with them -- they are
     deliberately the same NUMBERS, because a grid cannot simply
     inherit a table's rules. */
  .aiu-req-head, .aiu-req-row { display: grid; gap: .6rem; align-items: center; padding: .6rem .75rem; }
  .aiu-req-head { font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase;
                  color: var(--mpa-muted); background: var(--mpa-surface);
                  border-bottom: 1px solid var(--mpa-line); padding: .65rem .75rem; }
  .aiu-req-row { border-top: 1px solid var(--mpa-hairline); font-size: var(--mpa-text-md);
                 background: var(--mpa-surface); transition: background .12s; }
  .aiu-req-row:first-of-type { border-top: none; }
  .aiu-req-row:hover { background: var(--mpa-row-hover); }
  .aiu-req-cell { min-width: 0; }
  .aiu-req-centre { text-align: center; }
  .aiu-req-head-label { display: inline-flex; align-items: center; justify-content: center; gap: .25rem; width: 100%; }
  .aiu-req-head-label.aiu-tip-target { cursor: help; border-radius: 3px; }
  .aiu-req-head-label.aiu-tip-target:hover { color: var(--mpa-ink); }
  .aiu-req-head-label.aiu-tip-target:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }
  .aiu-req-primary { display: block; font-weight: 600; font-variant-numeric: tabular-nums; }
  .aiu-req-primary[data-bs-toggle="tooltip"] { cursor: help; border-radius: 3px; }
  .aiu-req-primary[data-bs-toggle="tooltip"]:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }
  .aiu-req-sub { display: block; color: var(--mpa-muted); font-size: var(--mpa-text-sm); line-height: 1.3; margin-top: .05rem; }
  .aiu-req-cell .mpa-chip { font-size: var(--mpa-text-xs); margin-right: .2rem; cursor: help; }
  .aiu-req-cell .mpa-chip[tabindex="0"]:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }

  @media (max-width: 575.98px) {
    .aiu-kpi-wide { grid-column: 1 / -1; }
    .aiu-kpi { padding: .7rem .6rem; min-height: 4.5rem; gap: .15rem; }
    .aiu-kpi-value { font-size: 1.5rem; }
    .aiu-kpi-pair { gap: 1rem; }
    .aiu-kpi-pair-value { font-size: 1.25rem; }
    .aiu-kpi-grid2 { column-gap: .8rem; }
    .aiu-kpi-cache-value { font-size: var(--mpa-text-base); }
  }

  @media (max-width: 767.98px) {
    /* ── THE ONE STACKED-CARD ANATOMY (final visual corrective, 21 Sep
       2026). Each row becomes a card; each cell becomes ONE coherent
       line: its label in a fixed left column, its value(s) stacked in the
       right column and right-aligned. This replaced a flex
       space-between, which spread a cell's TWO children -- "Haiku 4.5"
       and "1 call", or the token count and its "cache read" sub -- to
       opposite edges of the row with the label in between, so the model
       and its call count read as unrelated. A grid keeps every value of
       a field together, on every one of the three pages, because this is
       the only place the rule lives. */
    .aiu-req-head { display: none; }
    .aiu-req-row { display: block; padding: .8rem 1rem; }
    .aiu-req-row .aiu-req-cell {
      display: grid; grid-template-columns: minmax(6.5rem, max-content) minmax(0, 1fr);
      column-gap: .75rem; align-items: baseline; padding: .22rem 0;
    }
    .aiu-req-row .aiu-req-cell > * { grid-column: 2; text-align: right; min-width: 0; }
    .aiu-req-row .aiu-req-centre { text-align: right; }
    .aiu-req-row .aiu-req-cell[data-label]::before {
      content: attr(data-label); grid-column: 1; grid-row: 1 / span 4;
      font-size: var(--mpa-text-2xs); font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
      color: var(--mpa-muted); align-self: start; padding-top: .15em;
    }
    /* A value stacked under its primary (a call count, a cache-read
       figure) sits directly beneath it on the same right edge. */
    .aiu-req-row .aiu-req-cell .aiu-req-sub { margin-top: .05rem; }
    /* The when/school header is the card's TITLE LINE, not a field: the
       time on the left, the row's status/outcome chip on the right, and
       the school/model line beneath. This was Tenant AI's accepted §C
       mobile header; it is now the header for all three lists, so the
       chip lives in the same place on every page and the labelled
       Status/Outcome row is not repeated underneath it. */
    .aiu-req-row .aiu-req-time { display: grid; grid-template-columns: 1fr auto;
                                 grid-template-areas: "when status" "sub sub";
                                 row-gap: .2rem; column-gap: .6rem; align-items: center;
                                 padding: 0 0 .45rem; margin-bottom: .15rem;
                                 border-bottom: 1px solid var(--mpa-hairline); text-align: left; }
    .aiu-req-row .aiu-req-time > * { text-align: left; }
    .aiu-req-row .aiu-req-time .aiu-req-primary { grid-area: when; }
    .aiu-req-row .aiu-req-time .aiu-req-sub { grid-area: sub; margin-top: 0; }
    .aiu-req-row .aiu-req-time .aiu-req-mobile-status { grid-area: status; display: block; text-align: right; }
    .aiu-req-row .aiu-req-status { display: none; }
  }
  /* The header chip exists only for the stacked card; on the desktop grid
     the status/outcome has its own column. */
  .aiu-req-mobile-status { display: none; }

  /* Clickable sort headers + pagination (the filter row itself is
     app.css's shared .mpa-filter-bar component). */
  .tai-sort-link { color: var(--mpa-muted); text-decoration: none; font-size: var(--mpa-text-xs); font-weight: 700;
                   letter-spacing: var(--mpa-tracking-label); text-transform: uppercase; padding: .6rem 0; display: block; }
  .tai-sort-link:hover { color: var(--mpa-ink-brand); }
  .tai-sort-link.active { color: var(--mpa-ink-brand); }
  /* ── §F: a sortable heading must LOOK sortable before you click it.
     Every sort link carries a faint chevron; the ACTIVE column's own
     caret (rendered in the markup) is the one that shows direction, so
     this placeholder is hidden there to avoid two arrows on one
     heading. Hover/focus brings it up to full strength. */
  .tai-sort-link:not(.active)::after {
    content: "\F282";                 /* bi-chevron-expand */
    font-family: bootstrap-icons; font-weight: 400; font-size: .85em;
    margin-left: .3rem; opacity: .32; vertical-align: -.05em;
  }
  .tai-sort-link:not(.active):hover::after,
  .tai-sort-link:not(.active):focus-visible::after { opacity: .85; }
  .tai-sort-link:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; border-radius: 3px; }
  .tai-sort-link .aiu-req-head-label { width: auto; }
  .tai-pagination { display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; justify-content: space-between;
                     padding: .8rem 1.1rem; border-top: 1px solid var(--mpa-hairline); }

/* ── Tooltip / explanation targets (was partials/_aiu_tips.html) ── */

  .aiu-tip-target { cursor: help; }
  .btn.aiu-tip-target, a.aiu-tip-target { cursor: pointer; }
  /* The discoverability that replaces the icon: the words themselves are
     marked as explainable. Deliberately faint -- a KPI label is not a
     link, and a full underline would read as one. Applied only to the
     label/heading text, never to a chip or a button (they already look
     like controls). */
  .aiu-kpi-label.aiu-tip-target, .aiu-req-head-label.aiu-tip-target,
  .aiu-tip-target > .aiu-kpi-label, .aiu-tip-target > .aiu-req-head-label {
    text-decoration: underline dotted currentColor 1px;
    text-underline-offset: 3px;
    text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  }
  .aiu-kpi-hint { font-size: .82em; opacity: .45; }
  /* ⚠ THE DESKTOP HIDE NOW LIVES IN app.css (21 Sep 2026), not here.
     This file is included at the BOTTOM of the page, so hiding the
     glyphs from here meant they painted on first render and disappeared
     a frame later -- a visible flash of the very icons that were meant
     to be gone, on every cold load. app.css is in <head>. The script
     below still REMOVES them from the DOM, which is what makes the label
     itself the tooltip target; this file no longer owns whether they are
     SEEN. */
  .aiu-kpi-hint-tap { border: none; background: none; padding: .15rem; margin: -.15rem; line-height: 1; color: inherit;
                      opacity: .75; border-radius: 3px; cursor: pointer; }
  .aiu-kpi-hint-tap:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 1px; }
  .aiu-info-icon { cursor: help; opacity: .55; }
  .aiu-info-icon:hover, .aiu-info-icon:focus { opacity: 1; }
  .mpa-chip.aiu-tip-target:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }

/* ── Admin Console list rows / allowance bars / cost-family rows ──
   Lifted verbatim from watchtower_home / watchtower_school /
   watchtower_tenant_ai / watchtower_tenant_ai_school, where each carried
   its own copy (the audit's 62 duplicated selectors). Only the per-page
   grid-template-columns stay in the templates, because the lists
   genuinely have different columns. The one real divergence found
   (.wt-ai-bar's margin-bottom, .2rem on two pages and .25rem on two) is
   resolved here to .25rem, the AI pages' value. Row hover is the house
   --mpa-row-hover token, not the literal #faf9fd the copies carried. */
.wt-list-head, .wt-row { display: grid; gap: .65rem; align-items: center; padding: .6rem 1.1rem; }
.wt-list-head { font-size: max(var(--mpa-text-xs), 10.5px); font-weight: 700; letter-spacing: var(--mpa-tracking-label); text-transform: uppercase; color: var(--mpa-muted); border-bottom: 1px solid var(--mpa-hairline); }
.wt-row { border-top: 1px solid var(--mpa-hairline); text-decoration: none; color: var(--mpa-ink); font-size: var(--mpa-text-md); background: var(--mpa-surface); }
.wt-row:first-of-type { border-top: none; }
.wt-row:hover, .wt-row:focus-visible { background: var(--mpa-row-hover); color: var(--mpa-ink); outline: none; }
.wt-row:focus-visible { box-shadow: inset 0 0 0 2px var(--mpa-ink-brand); }
.wt-row:hover .wt-name { color: var(--mpa-ink-brand); text-decoration: underline; }
.wt-name { font-weight: 700; display: block; }
.wt-sub { display: block; color: var(--mpa-muted); font-size: var(--mpa-text-sm); line-height: 1.3; }
/* Centred columns (everything except SCHOOL). text-align carries the
   inline content and the block-level .wt-sub lines; .wt-c-int is a flex
   row of chips so it needs justify-content as well, and the AI bar is a
   block element that must not be stretched by the centring. */
.wt-centre { text-align: center; }
.wt-c-int { display: flex; flex-wrap: wrap; gap: .25rem; }
.wt-c-int.wt-centre { justify-content: center; }
.wt-c-int .mpa-chip { font-size: var(--mpa-text-xs); }
.wt-ai { display: block; }
.wt-centre .wt-ai { margin-inline: auto; }
.wt-ai-bar { display: block; height: 6px; border-radius: 3px; background: var(--mpa-hairline); overflow: hidden; margin-bottom: .25rem; }
.wt-ai-fill { display: block; height: 100%; background: var(--mpa-ok); }
.wt-ai-fill.hot { background: var(--mpa-warn); }
.wt-ai-fill.over { background: var(--mpa-danger); }
@media (max-width: 991.98px) {
  .wt-row, .wt-list-head { grid-template-columns: 1fr 1fr; padding: .7rem .9rem; }
  .wt-row .wt-c-school { grid-column: 1 / -1; }
  /* Below the desktop grid the row becomes a stacked two-column card, so
     centring would fight the label/value reading order. */
  .wt-row .wt-centre { text-align: left; }
  .wt-row .wt-c-int.wt-centre { justify-content: flex-start; }
  .wt-row .wt-centre .wt-ai { margin-inline: 0; }
}
@media (max-width: 575.98px) {
  .wt-row .wt-centre[data-label]::before {
    content: attr(data-label); display: block; font-size: var(--mpa-text-2xs); font-weight: 700;
    text-transform: uppercase; letter-spacing: .05em; color: var(--mpa-muted); margin-bottom: .1rem;
  }
}
/* Cost-by-operation-family rows (Tenant AI list + school drill-down). */
.aiu-fam-row { display: grid; grid-template-columns: 9rem 1fr 5.5rem 4rem; gap: .6rem; align-items: center; padding: .35rem 0; font-size: var(--mpa-text-sm); }
.aiu-fam-label { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.aiu-fam-bar { display: block; height: 8px; border-radius: 4px; background: var(--mpa-hairline); overflow: hidden; }
.aiu-fam-fill { display: block; height: 100%; background: var(--mpa-ink-brand); }
.aiu-fam-cost { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.aiu-fam-ops { text-align: right; color: var(--mpa-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 575.98px) {
  .aiu-fam-row { grid-template-columns: 1fr auto auto; grid-template-areas: "label cost ops" "bar bar bar"; row-gap: .3rem; }
  .aiu-fam-label { grid-area: label; } .aiu-fam-bar { grid-area: bar; } .aiu-fam-cost { grid-area: cost; } .aiu-fam-ops { grid-area: ops; }
}

/* ============================================================
   UI-CONSISTENCY-1B — house rule corrective (22 Sep 2026)
   ============================================================
   PROJECT_CONTEXT §9.3.1 rules 7–10: the desktop explanation primitive
   (data-aiu-tip, hover/focus, no glyphs), the mobile priority rule (one
   compact record per row/card), the spacing rule and the footer rule.
   The blocks below were built as four parallel working stylesheets, one
   per page family, and merged here verbatim. */

/* ── UI-CONSISTENCY-1B (22 Sep 2026) — Dashboard / Students / Invoices / Overdue ── */
/* UI-CONSISTENCY-1B working file A (dashboard / students / invoices / overdue)
   -- merged into app.css at the end of the pass. */

/* §C2 Students phone list: ONE compact line per pupil (PROJECT_CONTEXT
   §9.3.1 rule 8). The name truncates with an ellipsis instead of wrapping
   the row into a tall card (the full name stays in the DOM and in the
   row's aria-label); chips stay on the same line and never shrink. */
.mpa-rows-compact .mpa-row { padding-top: .5rem; padding-bottom: .5rem; flex-wrap: nowrap; }
.mpa-rows-compact .mpa-row-main { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mpa-rows-compact .mpa-row-side { flex-shrink: 0; white-space: nowrap; }

/* §B4 / §D1 money figures: value centred over its label, one treatment on
   the dashboard Money card and the invoice list summary (rule 1). The
   hover target is the whole figure block, so give it a soft radius for
   the focus ring the explanation primitive draws. */
.mpa-money-figs .mpa-stat, .mpa-money-figs .mpa-stat-label { text-align: center; }
.mpa-money-figs > [data-aiu-tip] { border-radius: var(--mpa-radius-sm); }
.mpa-money-figs > [data-aiu-tip]:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }

/* §B2 / §B3 dashboard stat groups: the same soft focus treatment on the
   band / category columns that now carry the explanation. */
.mpa-card [data-aiu-tip].col:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; border-radius: var(--mpa-radius-sm); }

/* ── UI-CONSISTENCY-1B (22 Sep 2026) — Settings → Invoicing / Branding / Portal invites ── */

/* ── Settings → Invoicing (UI-CONSISTENCY-1B, stream B) ───────────────── */
/* F2: days-to-pay -- label + control on one line, control obviously the
   thing to edit; helper text beneath, never a dominating side column. */
.uic1b-days-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.uic1b-days-control { width: 9.5rem; flex: 0 0 auto; }
.uic1b-days-control .form-control { text-align: center; font-weight: 600; }
.uic1b-days-help { max-width: 48rem; }
/* F3: rule rows -- one control height (input-group-sm) in every cell, one
   width per column, top-aligned with the reserved Unsaved line kept. */
.uic1b-rules-card .uic1b-rule-input { width: 7.6rem; flex-wrap: nowrap; }
.uic1b-rules-card .uic1b-rule-input .form-control { min-width: 3.4rem; }
.uic1b-rules-card .mpa-table.table tbody td { padding-top: .55rem; padding-bottom: .35rem; }
/* F3/F4: the add-new-rule area is its own labelled section, not a fourth
   blank row; the toggle button sits alone until pressed. */
.uic1b-add-rule { margin-top: .25rem; }
.uic1b-add-rule #add-rule-form .mpa-card-title { margin-bottom: .1rem; }
/* F4: mobile rule cards -- same compact form, tighter rhythm. */
.uic1b-rule-cards .card > .card-body { padding: .6rem .75rem; }
@media (max-width: 575.98px) {
  .uic1b-days-row { align-items: center; }
  .uic1b-add-rule .col-auto { flex: 1 1 45%; }
  .uic1b-add-rule .col-auto .input-group { width: 100%; }
  .uic1b-add-rule .col-auto .form-control { max-width: none !important; }
}

/* ── Settings → Invoicing: monthly period preview (MONTHLY-INVOICING-V1, 24 Sep 2026) ── */
/* Read-only rows — no edit/delete control, no separate admin screen (build
   brief §4). Compact on every width; a subtle divider between rows is
   enough structure without a table. */
.mpa-cadence-preview-row { padding: .2rem 0; border-bottom: 1px solid var(--mpa-border-subtle, #eee); }
.mpa-cadence-preview-row:last-child { border-bottom: none; }

/* ── Settings → Branding: venue colours (UI-CONSISTENCY-1B, stream B) ── */
/* G3: below 992px the table is a .mpa-table-stack record per venue; the
   separate Colour column is desktop-only (the swatch joins the control
   row instead) and the control row carries no data-label, so a venue is
   exactly: name line, then ONE compact swatch / picker / hex / Save /
   reset line. Nothing wraps into a stray column, nothing clips. */
@media (max-width: 991.98px) {
  .uic1b-venue-colour-col { display: none !important; }
  .mpa-table-stack td.uic1b-venue-controls { display: block !important; padding-top: .35rem !important; }
  .uic1b-venue-row { flex-wrap: nowrap; gap: .5rem !important; }
  .uic1b-venue-row .font-monospace { min-width: 4.6rem; }
  .uic1b-venue-row #venue-saved-placeholder, .uic1b-venue-row span[id^="venue-saved-"] { margin-left: auto; white-space: nowrap; }
}

/* ── Portal invites (UI-CONSISTENCY-1B, stream B) ── */
/* M: the invite table restacks into a family card below 992px. The
   family line is the title; Email / Children / Status are labelled rows;
   the actions row is a left-aligned button group so Send invite / Review
   access / Revoke invite stay visible, full-width on a phone. */
@media (max-width: 991.98px) {
  .uic1b-invites.mpa-table-stack td.uic1b-invite-actions { justify-content: flex-start; padding-top: .45rem !important; }
  .uic1b-invites.mpa-table-stack td.uic1b-invite-actions > div { display: flex; flex-wrap: wrap; gap: .4rem; width: 100%; justify-content: flex-start; }
  .uic1b-invites.mpa-table-stack td.uic1b-invite-actions form { display: block; flex: 1 1 8rem; }
  .uic1b-invites.mpa-table-stack td.uic1b-invite-actions .btn { width: 100%; }
  .uic1b-invites.mpa-table-stack td[data-label="Children"] > div,
  .uic1b-invites.mpa-table-stack td[data-label="Status"] > div { flex-basis: 100%; text-align: right; margin-top: 0 !important; }
  .uic1b-invites.mpa-table-stack td.text-center.small { font-size: var(--mpa-text-sm); }
}
@media (max-width: 991.98px) {
  .uic1b-invites.mpa-table-stack td[data-label="Children"] .uic1b-children { text-align: right; max-width: 100%; }
}

/* ── UI-CONSISTENCY-1B (22 Sep 2026) — Admin Console phone cards / AI lists ── */

/* ════════════════════════════════════════════════════════════════
   ADMIN CONSOLE PHONE CARDS (UI-CONSISTENCY-1B §I / §J, 22 Sep 2026)
   ════════════════════════════════════════════════════════════════
   Below 768px the Admin Console school list (watchtower_home) and the
   Tenant AI school list were the desktop grid squeezed into two
   unlabelled columns: "Today", "$0.88 of $10.00 · 9%", "Jul 2026" and a
   chip column running off the right edge. PROJECT_CONTEXT §9.3.1 rule 8:
   one record = one compact card -- identity, then status/action, then a
   deliberate two-column micro-grid of LABELLED facts. The desktop grid
   (>=768px) is untouched: these rules only redefine the phone layout the
   shared app.css rules already give .wt-row below 992px.
   The markup is the same anchor + cells; every cell now carries a
   data-label so nothing on a phone is an unexplained number. */
@media (max-width: 767.98px) {
  .wt-list .wt-list-head { display: none; }
  /* a.wt-row (0,1,1) deliberately outweighs the page-local
     `.wt-list-head, .wt-row { grid-template-columns: <8 desktop columns> }`
     (0,1,0) that each Console page declares in its own <style> block --
     which, loading after every stylesheet, was silently keeping the EIGHT
     desktop tracks alive on phones and squeezing each fact into a
     1/8-width column. */
  a.wt-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: .9rem;
    row-gap: .4rem;
    padding: .7rem 1rem .75rem;
    align-items: start;
  }
  a.wt-row > * { min-width: 0; text-align: left; }
  .wt-row .wt-c-school { grid-column: 1 / -1; }
  .wt-row .wt-c-school .wt-name { font-size: var(--mpa-text-base); line-height: 1.25; overflow-wrap: anywhere; }
  .wt-row .wt-c-school .wt-sub { overflow-wrap: anywhere; }
  /* Every labelled fact: small uppercase label ABOVE the value, so the
     value can be a chip, a bar, a date or a figure without a column to
     explain it. */
  .wt-row .wt-centre[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: var(--mpa-text-2xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--mpa-muted);
    margin-bottom: .15rem;
  }
  .wt-row .wt-centre { text-align: left; }
  .wt-row .wt-centre .wt-ai { margin-inline: 0; }
  .wt-row .wt-centre .mpa-chip { max-width: 100%; white-space: normal; }
  /* Chips row (integrations) must wrap inside its own cell, never run off. */
  .wt-row .wt-c-int { display: flex; flex-wrap: wrap; gap: .2rem; align-content: start; }
  .wt-row .wt-c-int.wt-centre::before { flex-basis: 100%; }
  .wt-row .wt-c-int.wt-centre { justify-content: flex-start; }
  /* The one card that is the row's STATUS gets the full width so its
     problems line reads as a sentence, not a squeezed column. */
  .wt-row .wt-c-status { grid-column: 1 / -1; }
  .wt-row .wt-c-status .wt-sub { white-space: normal; }
  /* The AI allowance bar is a gauge: full width of its own cell. */
  .wt-row .wt-c-ai { grid-column: 1 / -1; }
  .wt-row .wt-c-ai .wt-ai-bar { max-width: 14rem; }
  .wt-row .wt-c-billing { grid-column: auto; }
  .wt-row .wt-c-joined { text-align: left !important; }
  .wt-row .wt-c-joined .mpa-chip { margin-left: .3rem; }
}
/* Tenant AI list: the mode chip sits on the NAME line (School name + AI
   state chip), flags underneath -- the brief's anatomy for the card. */
@media (max-width: 767.98px) {
  .tai-cols.wt-row .wt-c-school { display: flex; flex-wrap: wrap; align-items: center; column-gap: .5rem; row-gap: .15rem; }
  .tai-cols.wt-row .wt-c-school .wt-name { display: inline; }
  .tai-cols.wt-row .wt-c-school .wt-sub { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .35rem; }
  .tai-cols.wt-row .wt-centre[data-label="Allowance (month)"],
  .tai-cols.wt-row .wt-centre[data-label="Last activity"] { grid-column: 1 / -1; }
  .tai-cols.wt-row .wt-centre[data-label="Allowance (month)"] .wt-ai-bar { max-width: 14rem; }
}

/* ── AI list stacked card: ONE divider per record (§J) ──────────────
   The shared anatomy (app.css, from partials/_ai_console.html) drew a
   hairline under the when/school/model header INSIDE each card, so a
   record looked like the end of one card and the start of another. Only
   the divider BETWEEN records remains (the row's own border-top). Applies
   to Admin Console AI, Tenant AI and Support AI alike -- same partial. */
@media (max-width: 767.98px) {
  .aiu-req-row .aiu-req-time {
    border-bottom: 0;
    padding-bottom: .2rem;
    margin-bottom: .05rem;
  }
  /* ── §K: a chip in a label/value row is a compact right-aligned value,
     never a full-width bar. A grid item stretches by default; a chip is
     content-sized and sits at the end of its column. */
  .aiu-req-row .aiu-req-cell > .mpa-chip { justify-self: end; width: auto; }
  /* The wrapper fills the value column (one left edge for every value, the
     accepted anatomy); the chip inside it is inline, content-sized and
     right-aligned by the column's own text-align. */
  .aiu-req-row .aiu-req-cell > .aiu-req-value { display: block; text-align: right; }
  .aiu-req-row .aiu-req-cell > .aiu-req-value > .mpa-chip { display: inline-flex; }
}

/* ── UI-CONSISTENCY-1B (22 Sep 2026) — Classes / Terms / Attendance / Staff / Events / Reports ── */
/* UI-CONSISTENCY-1B working file d -- merged into app.css at the end of the pass.
   Stream D: Terms / Attendance / Products / Events / Staff detail / Reports. */

/* ── Terms mobile rows (§N2) ── */
.mpa-term-row .mpa-row-main { line-height: 1.3; }
.mpa-term-row .mpa-term-row-name { font-weight: 600; color: var(--mpa-ink-brand); }
.mpa-term-row .mpa-term-row-dates { margin-top: .1rem; }
.mpa-term-row .mpa-row-side { margin-left: .5rem; }

/* ── Attendance mobile records (§N3): one compact record per session ── */
.mpa-att-mrows .mpa-att-mrow { padding: .55rem 1rem .55rem .95rem; align-items: center; border-left: 3px solid transparent; }
.mpa-att-mrows .mpa-att-mrow-today { border-left-color: var(--mpa-action); background: var(--mpa-tint); }
.mpa-att-mrows .mpa-att-mrow-today:hover { background: var(--mpa-tint-strong); }
.mpa-att-mrows .mpa-att-mrow-line { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .45rem; font-size: var(--mpa-text-md); }
.mpa-att-mrows .mpa-att-mrow-time { font-size: var(--mpa-text-sm); font-variant-numeric: tabular-nums; }
.mpa-att-mrows .mpa-att-mrow-meta { margin-top: .15rem; line-height: 1.35; }
.mpa-att-mrows .mpa-att-mrow-meta .mpa-chip { font-size: var(--mpa-text-2xs); padding: .1rem .45rem; vertical-align: baseline; }
.mpa-att-mrows .mpa-row-side { margin-left: .6rem; flex-shrink: 0; }
.mpa-att-mrows .mpa-att-action-btn { white-space: nowrap; }

/* ── Staff detail: Performance summary figures as house KPI tiles (§N4) ── */
.mpa-staff-perf-kpis .mpa-kpi { padding: .8rem .75rem; min-height: 0; }
.mpa-staff-perf-kpis .mpa-kpi-value { font-size: var(--mpa-text-xl); }
.mpa-staff-perf-kpis .mpa-kpi-note { margin-top: .1rem; }

/* ── Reports (§O1/§O3) ── */
/* One coherent desktop row: every control now stacks label-above-control
   (From / To used to sit inline beside their inputs while Term / Compare
   stacked), so all five controls share one baseline where width permits;
   the apply/clear pair sits level with the controls, not the labels. */
.mpa-report-filters .mpa-report-filter-actions { display: inline-flex; gap: .35rem; align-items: center; align-self: flex-end; }
/* Phone: the five report areas as a compact two-column grid instead of a
   strip that scrolls sideways (the fifth spans both columns). */
@media (max-width: 767.98px) {
  .mpa-tabs.mpa-report-tabs { display: grid; grid-template-columns: 1fr 1fr; gap: .25rem; overflow: visible; white-space: normal; padding: .25rem; }
  .mpa-tabs.mpa-report-tabs .nav-item { display: block; min-width: 0; }
  .mpa-tabs.mpa-report-tabs .nav-item:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .mpa-tabs.mpa-report-tabs .nav-link { display: flex; align-items: center; justify-content: center; text-align: center; white-space: normal; width: 100%; padding: .45rem .5rem; line-height: 1.2; }
}

/* ── Attendance desktop table at 992–1199px (§P): the seven fixed pixel
   columns sum to 1010px, 5px wider than the card at a 1024px viewport, so
   the whole document scrolled sideways (audit §13.7). The empty action
   column gives up 15px there; every other width is untouched. ── */
@media (min-width: 992px) and (max-width: 1199.98px) {
  .mpa-att-class-head ~ .mpa-card-body .mpa-table thead th:last-child { width: 105px !important; }
}

/* ── UI-CONSISTENCY-1B: wide house tables on a tablet ──
   A table-layout:fixed table with ten percentage columns squeezes its
   chips into 58px cells at 768px ("11 siblings" over "Active" -- 38
   overlapping chip pairs measured on Students). .mpa-table-wide gives such
   a table a real floor below 992px so it scrolls inside its wrapper
   instead of squeezing; desktop is untouched. */
@media (max-width: 991.98px) {
  .table-responsive:has(.mpa-table) > .mpa-table.table.mpa-table-wide { min-width: 60rem; }
}

/* ── UI-CONSISTENCY-1B: the two data surfaces that still pushed or clipped
   at tablet/small-desktop widths in the section-P sweep. Both keep their
   desktop (>= 1200px / >= 992px) rendering exactly as accepted. ── */
/* Class Interest: eleven columns + a two-button action cell. Below 1200px
   the wrapper scrolls (sticky header given up there, as on phones) instead
   of the document growing 51px wider than the viewport at 1024px. */
@media (max-width: 1199.98px) {
  .table-responsive:has(.mpa-table-scroll-lg), .mpa-card:has(.mpa-table-scroll-lg) { overflow-x: auto; overflow-y: visible; }
  .table-responsive:has(.mpa-table-scroll-lg) > .mpa-table-scroll-lg { min-width: 72rem; }
}
/* Support AI recent-questions grid: ten tracks with fixed minimums (900px)
   on a 768px tablet clipped 24 header/cell elements. Between the phone
   stack (< 768px) and the desktop grid (>= 992px) the list scrolls inside
   its card body. */
@media (min-width: 768px) and (max-width: 991.98px) {
  .mpa-card-body:has(> .sai-req-cols) { overflow-x: auto; }
  .sai-req-cols { min-width: 56rem; }
}

/* ============================================================
   UI-CONSISTENCY-1B — final acceptance corrective (22 Sep 2026)
   ============================================================
   Vertical rhythm on the tokens (PROJECT_CONTEXT §9.3.1 rule 11), the
   shared money anatomy (.mpa-money / .mpa-money-figs / .mpa-money-note /
   .mpa-money-progress / .mpa-meter) used by Dashboard, Invoices and
   Reports, lifecycle chip explanations, the Admin Console phone card's
   status-on-the-name-line, portal-invite label/value records, the
   attendance two-row record and the Reports 2-column phone nav. Built as
   three parallel working stylesheets and merged here verbatim. */

/* ── UI-CONSISTENCY-1B final acceptance corrective (22 Sep 2026) — Dashboard / Students / Invoices / Overdue ── */

/* ── Dashboard rhythm (final corrective) ──
   The dashboard's own sections sit one BLOCK gap below the content above
   them (the attention cards / KPI row / money card already carry their
   own bottom padding), not the section gap every other page uses for a
   heading that follows a plain block. Dashboard-only classes; the shared
   --mpa-space-* tokens are untouched. */
.mpa-dashboard-head { margin-bottom: .45rem; }
.mpa-dashboard-attention-row, .mpa-dashboard-section { margin-top: var(--mpa-space-block); }
.mpa-dashboard-attention-row { margin-bottom: .35rem; }
.mpa-dashboard-section { margin-bottom: var(--mpa-space-label); }

/* ── Card head directly above a table (Students / Classes / Staff / any
   list card): the head's own bottom padding plus the first heading row's
   top padding read as a gap between "STUDENTS 127" and the column names.
   Same anatomy on every list card, so one rule. */
.mpa-card-head:has(+ .table-responsive), .mpa-card-head:has(+ .d-none.table-responsive),
.mpa-card-head:has(+ .mpa-rows), .mpa-card-head:has(+ .table-responsive + .mpa-rows) { padding-bottom: .25rem; }

/* ── Money component (dashboard Money card, /invoices summary, Reports
   Revenue & Cash): .mpa-money > figures (.mpa-money-figs, centred value
   over label) → one note line (.mpa-money-note) → Collection progress
   label row (.mpa-money-progress) → .mpa-meter. Same tokens, same
   rhythm on every page that shows the trio; no unused vertical space. */
.mpa-money-note { margin-top: .4rem; margin-bottom: .6rem; }
.mpa-money-progress { margin-bottom: .3rem; }
.mpa-money .mpa-meter { margin-bottom: 0; }

/* ── UI-CONSISTENCY-1B final acceptance corrective (22 Sep 2026) — Settings → Invoicing / Portal invites / Admin Console card ── */

/* ── Settings → Invoicing: Late Fee Rules on the house rhythm ──
   The rule rows were 52px tall because the reserved "Unsaved" indicator
   lived on a second line under Save/Delete; it is inline now, so a row is
   one control line. The Save → "Late Fee Rules" gap was a 40px blank strip
   (mb-4 + an <hr class="my-4">); it is now the section token. */
.uic1b-late-fees-head { margin-top: var(--mpa-space-section); margin-bottom: var(--mpa-space-label); }
.uic1b-rules-card .mpa-table.table tbody td { padding-top: .45rem; padding-bottom: .45rem; }
.uic1b-rules-card .js-unsaved-badge[data-dirty-reserve] { display: inline-flex; align-items: center; line-height: 1; }
/* The last rule row's own bottom hairline IS the divider; a second border
   on the add area drew a double line. */
.uic1b-rules-card .uic1b-add-rule { margin-top: .35rem; border-top: 0 !important; padding-top: .3rem !important; }

/* ── Admin Console home phone card: identity left, principal status right ──
   The Status cell becomes `display: contents` below 768px so its chip can sit
   on the NAME line (grid column 2, right-aligned) and its problems line can
   run full width beneath -- the status is never its own labelled row. */
@media (max-width: 767.98px) {
  /* 1.35fr / 1fr: the identity column gets the larger share so a school
     name wraps less; the labelled pairs beneath (billing | sign-in,
     integrations | joined) tolerate the uneven split. */
  /* Scoped to the HOME list (a.wt-row without .tai-cols): the Tenant AI
     list keeps its own accepted name-line + chip flex anatomy. */
  a.wt-row:not(.tai-cols) { padding: .6rem .9rem .65rem; row-gap: .3rem; column-gap: .8rem; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .wt-row:not(.tai-cols) .wt-c-school { grid-column: 1; grid-row: 1; }
  .wt-row .wt-c-status { display: contents; }
  .wt-row .wt-c-status.wt-centre::before { content: none; }
  .wt-row .wt-c-status > .mpa-chip { grid-column: 2; grid-row: 1; justify-self: end; align-self: start; margin-top: .05rem; white-space: nowrap; max-width: 100%; }
  .wt-row .wt-c-status > .wt-sub { grid-column: 1 / -1; grid-row: 2; white-space: normal; margin-top: -.1rem; }
  .wt-row .wt-centre[data-label]::before { margin-bottom: .05rem; }
  .wt-row .wt-c-billing, .wt-row .wt-c-signin, .wt-row .wt-c-int, .wt-row .wt-c-joined { grid-row: auto; }
  .wt-row .wt-c-ai { grid-column: 1 / -1; }
  .wt-row .wt-c-school .wt-sub { line-height: 1.25; }
  /* Three integration chips on ONE line inside their cell. */
  .wt-row .wt-c-int .mpa-chip { font-size: .62rem; padding: .12rem .42rem; }
  .wt-row .wt-c-int { gap: .15rem; }
}

/* ── Portal invites phone record: one label | value anatomy ──
   Every stacked cell is a two-column grid: a fixed label column (EMAIL /
   CHILDREN / STATUS) and a left-aligned value column; secondary notes
   (mismatch / not-yet-visible / expiry) stack inside the value column
   directly beneath their value, so nothing becomes an orphan paragraph. */
@media (max-width: 991.98px) {
  .uic1b-invites.mpa-table-stack tbody tr { padding: .55rem 1rem .6rem; }
  .uic1b-invites.mpa-table-stack tbody tr:first-child { padding-top: .35rem; }
  .uic1b-invites.mpa-table-stack td:not(:first-child)[data-label] {
    display: grid !important;
    grid-template-columns: 5.2rem minmax(0, 1fr);
    column-gap: .6rem; row-gap: .15rem;
    align-items: baseline; justify-content: start;
    text-align: left !important;
    padding: .2rem 0 !important;
  }
  .uic1b-invites.mpa-table-stack td:not(:first-child)[data-label]::before { grid-column: 1; grid-row: 1; align-self: baseline; padding-top: 0; }
  .uic1b-invites.mpa-table-stack td:not(:first-child)[data-label] > * { grid-column: 2; text-align: left; margin-top: 0 !important; flex-basis: auto; max-width: 100%; overflow-wrap: anywhere; }
  .uic1b-invites.mpa-table-stack td[data-label="Children"] .uic1b-children { text-align: left; display: block; }
  .uic1b-invites.mpa-table-stack td[data-label="Children"] > div,
  .uic1b-invites.mpa-table-stack td[data-label="Status"] > div { font-size: var(--mpa-text-sm) !important; color: var(--mpa-muted); line-height: 1.3; }
  .uic1b-invites.mpa-table-stack td.text-center.small { font-size: var(--mpa-text-md); }
  .uic1b-invites.mpa-table-stack td:first-child { padding-bottom: .3rem !important; font-size: var(--mpa-text-base); }
  .uic1b-invites.mpa-table-stack td.uic1b-invite-actions { padding-top: .4rem !important; }
  /* A chip is a content-sized value, never a full-width bar; a long email
     breaks inside the card instead of running past its edge. */
  .uic1b-invites.mpa-table-stack td[data-label="Status"] > .mpa-chip { justify-self: start; width: auto; }
  .uic1b-invites.mpa-table-stack td[data-label="Email"] > .uic1b-email { display: block; overflow-wrap: anywhere; }
}

/* ── UI-CONSISTENCY-1B final acceptance corrective (22 Sep 2026) — Attendance two-row records / Reports rhythm, nav, money, KPIs ── */

/* ── §9 Attendance phone record: two aligned rows ──
   Row 1: date · time · Today marker (left) | state chip (right).
   Row 2: venue · room + register figure/exceptions (left) | action (right).
   The .mpa-rows block only renders below 768px (d-md-none), so these
   selectors never reach the desktop table. */
.mpa-att-mrows .mpa-att-mrow {
  display: flex; flex-direction: column; align-items: stretch; gap: .25rem;
  padding: .5rem 1rem .55rem .95rem;
}
.mpa-att-mrows .mpa-att-r1, .mpa-att-mrows .mpa-att-r2 {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem; min-width: 0;
}
.mpa-att-mrows .mpa-att-when { display: inline-flex; align-items: baseline; gap: .45rem; min-width: 0; font-size: var(--mpa-text-md); }
.mpa-att-mrows .mpa-att-today {
  font-size: var(--mpa-text-2xs); font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--mpa-ink-brand);
}
.mpa-att-mrows .mpa-att-state { flex-shrink: 0; }
.mpa-att-mrows .mpa-att-state .mpa-chip { white-space: nowrap; }
/* .mpa-rows .mpa-stat-note (app.css) pads a standalone note by 1.25rem; inside
   the record the venue line must start where the date does. */
.mpa-att-mrows .mpa-att-mrow-meta { margin-top: 0; padding: 0; min-width: 0; line-height: 1.35; overflow-wrap: anywhere; }
.mpa-att-mrows .mpa-att-r2 .mpa-row-side { margin-left: 0; flex-shrink: 0; }

/* ── §10B Report navigation on a phone: the Settings navigation pattern
   (.mpa-tabs.settings-group-nav) -- a deterministic two-column grid with
   44px cells, the fifth area on its own full-width row; no strip, no
   sideways scroll. Higher specificity than the earlier .mpa-report-tabs
   block in app.css by construction. ── */
@media (max-width: 767.98px) {
  ul.mpa-tabs.mpa-report-tabs {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px; width: 100%;
    overflow: visible; white-space: normal; padding: 4px;
  }
  ul.mpa-tabs.mpa-report-tabs .nav-item { display: contents; }
  ul.mpa-tabs.mpa-report-tabs .nav-item:last-child:nth-child(odd) .nav-link { grid-column: 1 / -1; }
  ul.mpa-tabs.mpa-report-tabs .nav-link {
    display: flex; align-items: center; justify-content: center; text-align: center; width: auto;
    min-height: 44px; padding: .4rem .45rem; font-size: var(--mpa-text-sm); line-height: 1.2; white-space: normal;
  }
  /* Text-only cells: with a two-line label the glyph floated to the far
     left while the words centred, which read as two different objects. */
  ul.mpa-tabs.mpa-report-tabs .nav-link .bi { display: none; }
}

/* ── §10D Report KPI cards on the dashboard principle: the card itself is
   the explanation target; notes centre with the figure and label. ── */
.mpa-kpi-row .mpa-kpi[data-aiu-tip] { cursor: help; }
.mpa-kpi-row a.mpa-kpi[data-aiu-tip] { cursor: pointer; }
.mpa-kpi-row .mpa-kpi-note { text-align: center; }
.mpa-kpi-row .mpa-kpi.aiu-tip-target { text-decoration: none; }
.mpa-kpi-row .mpa-kpi.aiu-tip-target .mpa-kpi-label { text-decoration: underline dotted currentColor 1px; text-underline-offset: 3px; text-decoration-color: color-mix(in srgb, currentColor 35%, transparent); }


/* ══════════════════════════════════════════════════════════════════════
   OVERDUE-INVOICES-V2 (22 Sep 2026) — selection + actions on the Overdue
   Invoices list. Everything below is SCOPED to .mpa-ovd-list (the overdue
   table) so the banked UI-CONSISTENCY-1/1B measurements of every other
   list are untouched. Two jobs:

   1. A SELECTED row is visibly selected (§9.3.1 rule 2 allows an
      intentional accent tint for selected/active). The brand wash
      --mpa-tint is set on the row's own --bs-table-bg so Bootstrap's cell
      paint picks it up on desktop; on the phone stack the <tr> carries its
      own background and the cells are transparent, so the same token is
      set there directly. is-selected is toggled by static/js/bulk_select.js
      from the checkbox state — one source.

   2. Below 992px the generic .mpa-table-stack record (first cell = bold
      title line, other cells = label:value rows) would make the CHECKBOX
      the title line. .mpa-ovd-list overlays a two-column grid on the same
      stacked record: checkbox in column 1, every other cell in column 2,
      Student promoted to the bold title, the actions cell a full-width
      wrap row of tap targets. Same DOM, same server values, no second
      row template (the "one table, restacked" contract UI-CONSISTENCY-1
      set for this page). Rule 8: the record shows identity, status/
      action, and the two facts needed to decide (days overdue, total);
      the Applicable-fee row is dropped when it is "—" (is-empty).
   ══════════════════════════════════════════════════════════════════════ */
.mpa-ovd-list tbody tr.is-selected {
  --bs-table-bg: var(--mpa-tint);
  --bs-table-hover-bg: var(--mpa-tint-strong);
}
.mpa-ovd-list tbody tr.is-selected > td { box-shadow: inset 0 0 0 9999px var(--mpa-tint); }
.mpa-ovd-list tbody tr.is-selected:hover > td { box-shadow: inset 0 0 0 9999px var(--mpa-tint-strong); }
/* Actions: buttons and the "why not" chips sit on one baseline, right-
   aligned, wrapping to a second line rather than clipping at 1024. */
.mpa-ovd-list td.ovd-c-actions {
  display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: .3rem;
}
.mpa-ovd-list td.ovd-c-actions form { display: inline; }
.mpa-ovd-list .ovd-why { cursor: help; }
/* Between 992 and 1199 the actions column is narrow: keep the envelope,
   drop the word, the hover/focus explanation still says what it does. */
@media (min-width: 992px) and (max-width: 1199.98px) {
  .mpa-ovd-list .ovd-btn-label { display: none; }
  /* Measured at 1024 (22 Sep 2026): chip + Void… + View need ~181px; the
     21% column gives 207px, so the 1rem right padding is trimmed to keep
     the three on one line and the row at the 37px house height. */
  .mpa-ovd-list th:last-child, .mpa-ovd-list td.ovd-c-actions { padding-right: .5rem !important; }
  .mpa-ovd-list td.ovd-c-actions { gap: .25rem; }
}
/* Sticky-first-column freeze (the generic .mpa-table tablet rule) would
   freeze the checkbox; on this table column 2 (Student) is the anchor. */
@media (max-width: 991.98px) {
  .mpa-ovd-list tbody tr {
    display: grid !important;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: .6rem; row-gap: 0;
    align-items: start;
  }
  .mpa-ovd-list tbody tr.is-selected { background: var(--mpa-tint); }
  .mpa-ovd-list tbody tr.is-selected > td { box-shadow: none; }
  .mpa-ovd-list tbody td { width: auto !important; min-width: 0; }
  .mpa-ovd-list tbody td.ovd-c-check { grid-column: 1; grid-row: 1; padding-top: .1rem !important; }
  .mpa-ovd-list tbody td.ovd-c-check .form-check-input { margin: 0; width: 1.15em; height: 1.15em; }
  /* Student is the title line (what td:first-child gives every other stacked table). */
  .mpa-ovd-list tbody td.ovd-c-student {
    grid-column: 2; grid-row: 1;
    display: block; font-weight: 700; font-size: var(--mpa-text-base); padding-bottom: .4rem !important;
  }
  .mpa-ovd-list tbody td.ovd-c-student::before { content: none; }
  .mpa-ovd-list tbody td:not(.ovd-c-check):not(.ovd-c-student) { grid-column: 2; }
  .mpa-ovd-list tbody td.ovd-c-fee.is-empty { display: none; }
  /* Actions: a full-width row of equal tap targets under the record, with
     the "why not" chips reading as plain notes rather than buttons. */
  .mpa-ovd-list tbody td.ovd-c-actions {
    justify-content: flex-start; gap: .4rem; margin-top: .45rem; padding-top: .45rem !important;
    border-top: 1px solid var(--mpa-hairline) !important;
  }
  .mpa-ovd-list tbody td.ovd-c-actions::before { content: none; }
  /* Mobile polish (24 Sep 2026, Rory): the actions row was visually too
     dominant — three full-width 38px blocks per record. House mobile
     action treatment instead: compact btn-sm buttons sized to their label
     (Remind primary-outline, Void danger-outline, View quiet), one row,
     right-aligned under the record, with a 34px minimum hit area kept
     (WCAG 2.5.8 minimum target) — smaller visually, still safe to tap. */
  .mpa-ovd-list tbody td.ovd-c-actions { justify-content: flex-end; gap: .35rem; margin-top: .4rem; padding-top: .4rem !important; }
  .mpa-ovd-list tbody td.ovd-c-actions form { display: inline-flex; flex: 0 0 auto; }
  .mpa-ovd-list tbody td.ovd-c-actions .btn {
    flex: 0 0 auto; min-height: 34px; padding: .3rem .7rem; font-size: var(--mpa-text-sm); line-height: 1.2;
  }
  .mpa-ovd-list tbody td.ovd-c-actions .btn.btn-outline-secondary { color: var(--mpa-muted); border-color: var(--mpa-line); }
  .mpa-ovd-list tbody td.ovd-c-actions .ovd-why { flex: 0 0 auto; align-self: center; margin-right: auto; }
  .mpa-ovd-list tfoot td.ovd-f-pad { display: none; }
  .mpa-ovd-list tfoot tr { display: block; }
}

/* ── OVERDUE-INVOICES-V2 corrective (24 Sep 2026): invoice-detail line
   typography. One family for every line type: the same base size, weight
   and row rhythm for class rows, sibling/multi-class and term discounts,
   the payment admin fee and the bank-transfer discount. The LINE TYPE is
   carried by semantic tint (the row's table-warning / table-success from
   the template) and by the amount's colour — never by italics, a second
   text colour on the description, or a different size. Calculations are
   untouched; this is presentation only. */
.mpa-invoice-lines tbody td {
  font-size: var(--mpa-text-md);
  font-weight: 400;
  font-style: normal;
  line-height: 1.35;
  padding-top: .5rem; padding-bottom: .5rem;
  vertical-align: middle;
}
.mpa-invoice-lines tbody td.mpa-line-desc { color: var(--mpa-ink); }
.mpa-invoice-lines tbody td.mpa-line-desc a { font-weight: 600; }
.mpa-invoice-lines tbody td.mpa-line-desc .fw-semibold { font-weight: 600; }
/* Amounts share one numeric treatment; discount amounts stay green, the fee
   amount stays amber — colour is the only variable. */
.mpa-invoice-lines tbody td:nth-child(2), .mpa-invoice-lines tbody td:nth-child(3), .mpa-invoice-lines tbody td:nth-child(4) {
  font-variant-numeric: tabular-nums;
}
.mpa-invoice-lines tbody tr.table-warning td:nth-child(4) { color: var(--mpa-warn-ink); font-weight: 600; }
.mpa-invoice-lines tbody tr.table-success td:nth-child(4) { color: var(--mpa-ok-ink); font-weight: 600; }
.mpa-invoice-lines tbody tr.table-success td .text-success { color: var(--mpa-ok-ink) !important; }
/* A small, uniform type label on the non-class lines replaces the italic
   description as the "this is a fee / discount" signal. */
.mpa-invoice-lines tbody td.mpa-line-admin_fee::after,
.mpa-invoice-lines tbody td.mpa-line-bank_transfer_discount::after,
.mpa-invoice-lines tbody td.mpa-line-discount::after {
  display: inline-block; margin-left: .45rem; padding: .05rem .45rem; border-radius: 999px;
  font-size: var(--mpa-text-2xs); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; vertical-align: middle;
}
.mpa-invoice-lines tbody td.mpa-line-admin_fee::after { content: "fee"; background: var(--mpa-warn-bg); color: var(--mpa-warn-ink); }

/* ── OVERDUE-INVOICES-V2 corrective (24 Sep 2026): the invoice toolbar's
   scoped bulk buttons read "off" until an approved row is ticked. Not
   Bootstrap's .disabled (pointer-events:none would silence the hover/
   focus explanation the button exists to show); the click handler is the
   real gate. Same visual weight as a disabled button, still hoverable. */
.mpa-bulk-scoped.mpa-bulk-off { opacity: .55; cursor: not-allowed; box-shadow: none; }
.mpa-bulk-scoped.mpa-bulk-off:hover { opacity: .65; }

/* ── OVERDUE-INVOICES-V2 final corrective (24 Sep 2026): dashboard ageing
   cells are links into the overdue workspace — the same house clickable
   treatment as .mpa-status-tile (pointer, hover tint, focus ring), the
   figure/label anatomy unchanged, no glyph. ── */
.mpa-ovd-band {
  display: block; width: 100%; padding: .35rem .25rem; border-radius: var(--mpa-radius-sm);
  border: 1px solid transparent; color: inherit; text-decoration: none; cursor: pointer;
  transition: background .16s ease, border-color .16s ease;
}
.mpa-ovd-band:hover { background: var(--mpa-tint); color: inherit; text-decoration: none; }
.mpa-ovd-band:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: 2px; }
.mpa-ovd-band .mpa-stat-label { text-decoration: underline dotted transparent 1px; text-underline-offset: 3px; }
.mpa-ovd-band:hover .mpa-stat-label { text-decoration-color: currentColor; }
.mpa-ovd-band-total { display: block; width: auto; margin: .5rem auto 0; padding: .25rem .6rem; }

/* Active fee tiers legend on the overdue workspace: one aligned row per
   tier (threshold left, amount right on a shared edge), the "display only"
   note as supporting copy — never a chip that wraps at 390. */
.mpa-ovd-tiers { display: grid; grid-template-columns: auto auto; column-gap: 1.25rem; row-gap: .15rem; align-items: baseline; width: max-content; max-width: 100%; }
.mpa-ovd-tiers .t { font-size: var(--mpa-text-sm); color: var(--mpa-ink); font-weight: 500; white-space: nowrap; }
.mpa-ovd-tiers .a { font-size: var(--mpa-text-sm); color: var(--mpa-warn-ink); font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.mpa-ovd-legend { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .35rem 1.5rem; }
.mpa-ovd-legend .mpa-card-title { margin-bottom: .1rem; }
.mpa-ovd-legend .mpa-ovd-legend-note { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin-left: auto; align-self: flex-end; }
@media (max-width: 767.98px) {
  .mpa-ovd-legend .mpa-ovd-legend-note { margin-left: 0; width: 100%; }
  /* Phone filter bar (measured 390, 24 Sep 2026): "Overdue period" is the
     one label longer than the stacked bar's 5.2rem label column, so this
     page's labels may wrap to two lines instead of clipping. */
  #overdue-filters .mpa-filter-field .form-label { width: 5.6rem; white-space: normal; line-height: 1.1; }
}
@media (max-width: 991.98px) {
  /* Phone record actions (measured 390, 24 Sep 2026): a "why not" note
     (Remind in QuickBooks / Under review / No reminder) takes its own line
     above the buttons, so Void… and View always sit together on one row. */
  .mpa-ovd-list tbody td.ovd-c-actions .ovd-why { flex: 1 1 100%; margin-right: 0; order: -1; }
}
.mpa-invoice-lines tbody td.mpa-line-bank_transfer_discount::after,
.mpa-invoice-lines tbody td.mpa-line-discount::after { content: "discount"; background: var(--mpa-ok-bg); color: var(--mpa-ok-ink); }

/* ── MONTHLY-INVOICING-V1 corrective (25 Sep 2026) ──────────────────────
   §N phone invoice card for a MONTHLY invoice: row 1 name · total (as
   before); row 2 the month chip (+ "1/2" only when that month really has
   more than one invoice) on the left, the lifecycle chips on the right. The
   term-level "Invoice N of M" line is not rendered for monthly invoices. */
@media (max-width: 575.98px) {
  .mpa-inv-list tbody td.inv-c-status.has-month { margin-top: .35rem; }
  .mpa-inv-list tbody td.inv-c-status .mpa-inv-month-m {
    display: inline-flex; gap: .3rem; align-items: center; order: -1; margin-right: auto;
  }
}
/* §Q: explanation targets never show the help (question-mark) cursor. The
   hover/focus explanation itself is unchanged; a target inside something
   clickable (a whole-row link, a button, a link) keeps that thing's
   pointer by inheriting it, plain explanatory text keeps the normal arrow. */
.aiu-tip-target, [data-aiu-tip],
.aiu-kpi.aiu-tip-target, .aiu-req-head-label.aiu-tip-target,
.aiu-req-primary[data-bs-toggle="tooltip"], .aiu-req-cell .mpa-chip,
.aiu-info-icon, .mpa-kpi-row .mpa-kpi[data-aiu-tip], .mpa-ovd-list .ovd-why { cursor: inherit; }
.btn.aiu-tip-target, a.aiu-tip-target, a.aiu-kpi.aiu-tip-target, a[data-aiu-tip], button[data-aiu-tip],
.mpa-kpi-row a.mpa-kpi[data-aiu-tip] { cursor: pointer; }
/* §O invoice detail lines: ONE readable size for every line on desktop —
   class names, adjustment descriptions and every amount (class totals,
   discount/fee/custom amounts) — with the numeric column in tabular
   figures at one weight. The line type is carried by the row's semantic
   tint and the amount's colour only; the tiny second-line "DISCOUNT" /
   "FEE" pill is gone (the description already says what the line is), so
   adjustment rows are the same height as class rows. Maths unchanged. */
.mpa-invoice-lines tbody td.mpa-line-admin_fee::after,
.mpa-invoice-lines tbody td.mpa-line-bank_transfer_discount::after,
.mpa-invoice-lines tbody td.mpa-line-discount::after { content: none; display: none; }
@media (min-width: 576px) {
  .mpa-invoice-lines tbody td.mpa-line-desc,
  .mpa-invoice-lines tbody td.mpa-line-desc a,
  .mpa-invoice-lines tbody td.mpa-line-desc .fw-semibold,
  .mpa-invoice-lines tbody td.mpa-line-desc .mpa-invoice-desc,
  .mpa-invoice-lines tbody td:nth-child(4),
  .mpa-invoice-lines tbody td:nth-child(4) span,
  .mpa-invoice-lines tbody td:nth-child(4) input[type="number"] {
    font-size: 1rem;
  }
  .mpa-invoice-lines tbody td:nth-child(4) span,
  .mpa-invoice-lines tbody td:nth-child(4) input[type="number"] {
    font-weight: 600; font-variant-numeric: tabular-nums;
  }
  .mpa-invoice-lines tbody tr.table-success td:nth-child(4) input[type="number"] { color: var(--mpa-ok-ink); }
  .mpa-invoice-lines tbody tr.table-warning td:nth-child(4) input[type="number"] { color: var(--mpa-warn-ink); }
}
/* §P "Add one-off line item": tighter heading, hint on the label row. */
.mpa-oneoff-card .mpa-oneoff-head { padding-top: .45rem; padding-bottom: .45rem; }
.mpa-oneoff-card .mpa-oneoff-body { padding-top: .6rem; padding-bottom: .7rem; }
.mpa-oneoff-card .mpa-oneoff-hint { font-size: var(--mpa-text-xs); white-space: nowrap; }

/* XERO-V1: the Xero card header's status text shrinks and wraps beside its
   icon instead of dropping below it on narrow screens. */
.mpa-xero-head-text { flex: 1 1 10rem; min-width: 0; }

/* Shared legal/help footer (staff + Admin Console via partials/_legal_footer,
   parent portal's duplicated copy) — XERO-V1 acceptance corrective, 28 Sep
   2026. Deliberately SECONDARY and NEUTRAL, never school branding (school
   colours are configurable and could fail contrast or compete with primary
   actions): #524e5e is 7.5:1 on --mpa-bg (AAA for small text; the old 75%-
   alpha grey was 6.4:1 at 9.75px). Hover/focus go to --mpa-ink with an
   underline; keyboard focus also gets a visible outline. The "User guide"
   book is an inline SVG whose path touches its viewBox edge — inline SVG
   defaults to overflow:hidden, which shaved off the right-hand page's
   anti-aliased stroke at this small size, so it is drawn overflow:visible
   with a real gap before the text. */
.mpa-legal-footer { font-size: var(--mpa-text-sm); color: #524e5e; line-height: 1.6; }
.mpa-legal-footer a { color: #524e5e; text-decoration: none; border-radius: 3px; }
.mpa-legal-footer a:hover,
.mpa-legal-footer a:focus-visible { color: var(--mpa-ink); text-decoration: underline; text-underline-offset: 2px; }
.mpa-legal-footer a:focus-visible { outline: 2px solid var(--mpa-ink); outline-offset: 2px; }
.mpa-legal-footer .mpa-legal-sep { color: var(--mpa-muted); margin: 0 .45rem; }
.mpa-legal-footer .help-footer-link { white-space: nowrap; }
.mpa-legal-footer .help-footer-link svg { overflow: visible; width: 1.1em; height: 1.1em; vertical-align: -.2em; margin-right: .3em; }

/* ============================================================
   BULK-JOBS-V1 (29 Sep 2026): background-work strip (every admin
   page, top of the content area) + the job page. A compact, quiet
   status line — never a blocking overlay, never a modal. Wraps on a
   phone; no fixed positioning (the two floating corners belong to
   back-to-top and the AI Assistant).
   ============================================================ */
.mpa-jobs-strip { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .75rem; }
.mpa-jobs-strip.is-empty { display: none; margin: 0; }
.mpa-job-pill {
  display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .6rem;
  padding: .45rem .75rem; border-radius: var(--mpa-radius-sm);
  background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  box-shadow: var(--mpa-shadow); font-size: var(--mpa-text-sm); color: var(--mpa-ink);
  min-width: 0; max-width: 100%;
}
.mpa-job-pill.is-running, .mpa-job-pill.is-waiting { border-color: var(--mpa-info-bg); }
.mpa-job-pill.is-warn { border-color: var(--mpa-warn-bg); background: #fffdf8; }
.mpa-job-pill-icon { display: inline-flex; align-items: center; color: var(--mpa-info); flex: 0 0 auto; }
.mpa-job-pill.is-waiting .mpa-job-pill-icon { color: var(--mpa-warn); }
.mpa-job-pill.is-ok .mpa-job-pill-icon { color: var(--mpa-ok); }
.mpa-job-pill.is-warn .mpa-job-pill-icon { color: var(--mpa-warn); }
.mpa-job-pill.is-info { border-color: var(--mpa-info-bg); }
.mpa-job-pill-icon .spinner-border-sm { width: .85rem; height: .85rem; border-width: .15em; }
.mpa-job-pill-text { color: inherit; text-decoration: none; flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; }
.mpa-job-pill-text:hover strong { text-decoration: underline; }
.mpa-job-pill-count { margin-left: .35rem; color: var(--mpa-muted); font-variant-numeric: tabular-nums; }
.mpa-job-pill-note { margin-left: .35rem; color: var(--mpa-muted); font-style: italic; }
.mpa-job-pill-bar {
  flex: 1 1 100%; height: .3rem; border-radius: 99px; background: var(--mpa-track); overflow: hidden;
}
.mpa-job-pill-bar > span { display: block; height: 100%; background: var(--mpa-info); transition: width .4s ease; }
.mpa-job-pill.is-waiting .mpa-job-pill-bar > span { background: var(--mpa-warn); }
.mpa-job-pill-action { font-weight: 600; text-decoration: none; white-space: nowrap; }
.mpa-job-pill-close {
  border: 0; background: transparent; color: var(--mpa-muted); padding: 0 .15rem; line-height: 1;
  font-size: 1.05rem; cursor: pointer;
}
.mpa-job-pill-close:hover { color: var(--mpa-ink); }
.mpa-job-issues li { padding: .4rem 0; border-top: 1px solid var(--mpa-hairline); }
.mpa-job-issues li:first-child { border-top: 0; }
.mpa-job-list-row { padding: .7rem 1rem; border-top: 1px solid var(--mpa-hairline); }
.mpa-job-list-row:first-child { border-top: 0; }
.mpa-job-list-row a { color: inherit; }
.mpa-job-detail { max-width: 52rem; }
/* The acceptance note that replaces the old "keep this page open" panel. */
.mpa-job-accepted { display: flex; flex-wrap: wrap; gap: .25rem .6rem; align-items: baseline; }
.mpa-job-accepted .small { overflow-wrap: anywhere; }
/* The bulk-send confirmation dialog (built by JS on first use). */
#mpaJobConfirm .modal-dialog { max-width: 30rem; }
@media (max-width: 575.98px) {
  #mpaJobConfirm .modal-dialog { margin: .75rem; }
  #mpaJobConfirm .modal-footer { flex-wrap: nowrap; }
  #mpaJobConfirm .modal-footer .btn { flex: 1 1 0; white-space: normal; }
}

/* BULK-JOBS-V1 polish: per-invoice breakdown rows on the job page. */
.mpa-job-row { padding: .45rem 0; border-top: 1px solid var(--mpa-hairline); }
.mpa-job-row:first-child { border-top: 0; }
.mpa-job-row-main { display: flex; justify-content: space-between; gap: .75rem; align-items: baseline; min-width: 0; }
.mpa-job-row-main a, .mpa-job-row-main > span:first-child { min-width: 0; overflow-wrap: anywhere; }
.mpa-job-row-amt { font-variant-numeric: tabular-nums; white-space: nowrap; }
.mpa-job-rows > summary { list-style: none; }
.mpa-job-rows > summary::-webkit-details-marker { display: none; }

/* BULK-JOBS-V1 polish 2: obvious chevron + compact one-line rows (desktop), natural wrap (phone). */
.mpa-job-chev { transition: transform .15s ease; color: var(--mpa-muted); }
.mpa-job-rows[open] .mpa-job-chev { transform: rotate(180deg); }
.mpa-job-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: .1rem .75rem; padding: .4rem 0;
  border-top: 1px solid var(--mpa-hairline); font-size: var(--mpa-text-sm); min-width: 0; }
.mpa-job-row:first-child { border-top: 0; }
.mpa-jr-name { flex: 1 1 100%; min-width: 0; overflow-wrap: anywhere; font-size: var(--mpa-text-md); }
.mpa-jr-amt { order: 2; margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }
.mpa-jr-term, .mpa-jr-parent, .mpa-jr-ledger, .mpa-jr-issue { min-width: 0; overflow-wrap: anywhere; }
@media (min-width: 768px) {
  .mpa-job-row { flex-wrap: nowrap; align-items: baseline; }
  .mpa-jr-name { flex: 0 1 17rem; }
  .mpa-jr-term { order: 1; flex: 0 1 12rem; }
  .mpa-jr-parent { order: 1; flex: 0 1 14rem; }
  .mpa-jr-ledger { order: 1; flex: 0 1 9rem; }
  .mpa-jr-issue { order: 1; flex: 1 1 auto; }
  .mpa-jr-amt { order: 2; flex: 0 0 auto; margin-left: auto; }
}

/* ============================================================
   MOBILE-UI-POLISH-V1 (29 Sep 2026) — consistency + responsive polish.
   Additive, presentation only. Each block names its consumers.
   ============================================================ */

/* Navigation/history-destination button (Invoices → Bulk activity): the
   quick-action-chip look (.mpa-action-btn) at .btn-sm size — white surface,
   hairline border, FULL-STRENGTH ink text, brand icon. Clearly enabled, and
   visibly a different KIND of action from the blue operation buttons. */
.btn.mpa-btn-nav {
  --bs-btn-color: var(--mpa-ink);
  --bs-btn-bg: var(--mpa-surface);
  --bs-btn-border-color: var(--mpa-line);
  --bs-btn-hover-color: var(--mpa-ink-brand);
  --bs-btn-hover-bg: var(--mpa-surface);
  --bs-btn-hover-border-color: var(--mpa-action);
  --bs-btn-active-color: var(--mpa-ink-brand);
  --bs-btn-active-bg: var(--mpa-hairline);
  --bs-btn-active-border-color: var(--mpa-action);
  font-weight: 600; box-shadow: var(--mpa-shadow); opacity: 1;
}
.btn.mpa-btn-nav i { color: var(--mpa-ink-brand); }

/* Card heads (Dashboard cards, Report Cards …): title may wrap, but the count
   chip and a trailing "View all" never shrink/wrap — so "ATTENDANCE NOT
   SUBMITTED | 256 | View all →" keeps its relationship at any width. */
.mpa-card-head > .mpa-card-title { min-width: 0; flex: 0 1 auto; }
.mpa-card-head > .mpa-chip { flex: 0 0 auto; }
.mpa-card-head > .mpa-link { flex: 0 0 auto; white-space: nowrap; }
/* Opt-in: a head whose title + badge can't share one row (AI Instructions +
   "Standing — applies every term") stacks instead of clipping. */
.mpa-card-head.mpa-card-head-wrap { flex-wrap: wrap; row-gap: .35rem; }
.mpa-card-head-wrap > .mpa-chip { white-space: normal; text-align: left; }

/* Page-head control clusters (Report Cards: "Report card progress" + Term):
   on a phone they share one full-width row and split it, never a 3-line button. */
@media (max-width: 767.98px) {
  .mpa-head-controls { width: 100%; flex-wrap: wrap; margin-left: 0 !important; gap: .5rem !important; }
  .mpa-head-controls > .btn { flex: 1 1 11rem; white-space: nowrap; }
  .mpa-head-controls > .mpa-term-block { flex: 1 1 9rem; margin-left: 0 !important; }
  .mpa-head-controls .mpa-term-block form, .mpa-head-controls .mpa-term-block select { width: 100% !important; }
}

/* Dashboard section row ("Needs attention · 377 … 15 Sep – 23 Nov 2026"): the heading and its
   count stay ATTACHED on the left; the term range sits on the right of the same row while it
   fits, and only wraps (still right-aligned, never re-attached to the count) on very narrow
   phones. */
@media (max-width: 575.98px) {
  .mpa-dashboard-section-row, .mpa-dashboard-attention-row { justify-content: flex-start; align-items: center; column-gap: .75rem; }
  .mpa-dashboard-section-label, .mpa-dashboard-attention-label { flex: 0 0 auto; }
  .mpa-dashboard-section-row .mpa-dashboard-section-date { margin-left: auto; text-align: right; white-space: nowrap; font-size: var(--mpa-text-sm); }
  /* Needs-attention card heads: drop the category icon on phones so "ATTENDANCE NOT SUBMITTED | 256 | View all →"
     gets the full row (the title stays readable, the count and View all stay attached). */
  .mpa-attention-grid .mpa-card-head > .mpa-card-title > i.bi:first-child { display: none; }
  .mpa-attention-grid .mpa-card-head { gap: .25rem; }
  /* tighter uppercase tracking so the longest heading fits one row at 375; below that it WRAPS
     (never clips), with the count and View all still attached on the right */
  .mpa-attention-grid .mpa-card-head > .mpa-card-title { letter-spacing: 0; white-space: normal; min-width: 0; flex: 0 1 auto; }
}
/* THE attention-card alignment standard (close-out, 30 Sep 2026): on phones every Needs-attention
   card's head, list rows and body share ONE inset (--mpa-attn-inset), so the category heading
   ("ATTENDANCE NOT SUBMITTED") and the item names beneath it start on the SAME pixel — and any
   future card placed in .mpa-attention-grid inherits that with no per-card override. Desktop keeps
   its icon-led heads unchanged. Proven by qa/test_mobile_ui_polish_v1.py (D-align). */
@media (max-width: 575.98px) {
  .mpa-attention-grid { --mpa-attn-inset: 1rem; }
  .mpa-attention-grid .mpa-card-head,
  .mpa-attention-grid .mpa-card-body,
  .mpa-attention-grid .mpa-row { padding-left: var(--mpa-attn-inset); padding-right: var(--mpa-attn-inset); }
  /* a row list nested INSIDE a padded card body (Timetable issues) bleeds back out by one inset,
     so its rows land on the same shared edge instead of being inset twice */
  .mpa-attention-grid .mpa-card-body > .mpa-rows { margin-left: calc(-1 * var(--mpa-attn-inset)); margin-right: calc(-1 * var(--mpa-attn-inset)); }
}


/* Events (and any adaptive row) — one compact meta line under the name. */
.mpa-row.mpa-row-adaptive .mpa-row-meta {
  flex: 1 1 100%; font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-weight: 500;
  overflow-wrap: anywhere; line-height: 1.35;
}

/* MOBILE-UI-POLISH-V1: a chip carrying a list of names (Report Card Progress "Waiting on N: …") wraps inside its row instead of running off-screen. */
.mpa-chip.mpa-chip-wrap{white-space:normal;max-width:100%;min-width:0;overflow-wrap:anywhere;text-align:left}
/* MOBILE-UI-POLISH-V1: the invoice/dashboard status tile label ("AWAITING PAYMENT", "BOOKKEEPING") was nowrap and 3-12px wider than its tile at 1440/390/360, clipping. Wrap only when it does not fit. */
.mpa-inv-status-grid .mpa-status-name{white-space:normal;flex-wrap:wrap;text-align:center;row-gap:0}

/* ── MOBILE-UI-POLISH-V1 (manual-acceptance correction): Report Cards ─────────── */
/* Report Cards list card: primary action on the right on desktop, full-width tap target on phones. */
.mpa-card-actions { display: flex; justify-content: flex-end; }
@media (max-width: 767.98px) { .mpa-card-actions > .btn { flex: 1 1 100%; } }
/* Class page: bulk toolbar (Generate AI drafts, Save all notes, Approve all). */
.mpa-rc-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
/* Per-student card head: name + link left, teacher/date meta right (desktop). */
.mpa-rc-cardhead { display: flex; align-items: flex-start; justify-content: space-between; gap: .25rem .75rem; margin-bottom: 1rem; }
.mpa-rc-meta { text-align: right; }
/* Per-draft actions: inline row on desktop. */
.mpa-rc-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; }
@media (max-width: 767.98px) {
  .mpa-rc-toolbar { display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
  .mpa-rc-toolbar > .btn { width: 100%; }
  .mpa-rc-toolbar > #generate-drafts-btn { grid-column: 1 / -1; }
  .mpa-rc-actions { flex: 1 1 100%; display: grid; grid-template-columns: 1fr 1fr; width: 100%; }
  .mpa-rc-actions > .btn { width: 100%; }
  .mpa-rc-cardhead { flex-wrap: wrap; margin-bottom: .6rem; }
  .mpa-rc-meta { flex: 1 1 100%; text-align: left; }
  .mpa-rc-fullbtn { width: 100%; }
  .mpa-rc-card > .mpa-card-body { padding: .85rem; }
  .mpa-rc-card .row.g-3 { --bs-gutter-y: .6rem; }
}
@media (max-width: 767.98px) {
  /* an odd lone last action (e.g. Skip with no Approve) spans the row rather than sitting half-width */
  .mpa-rc-actions > .btn:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* ── MOBILE-UI-POLISH-V1 (correction): Events mobile card ── */
.mpa-row.mpa-ev-row { display: block; padding: .8rem .9rem; }
.mpa-ev-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; }
.mpa-ev-name { font-weight: 700; color: var(--mpa-ink-brand); overflow-wrap: anywhere; min-width: 0; }
.mpa-ev-chips { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: .25rem; flex: 0 0 auto; }
.mpa-ev-facts { display: flex; flex-wrap: wrap; gap: .1rem .9rem; margin-top: .3rem; font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
.mpa-ev-facts > span { min-width: 0; overflow-wrap: anywhere; }
.mpa-ev-facts i { margin-right: .2rem; }
.mpa-ev-foot { display: flex; flex-wrap: wrap; align-items: center; gap: .15rem 1.1rem; margin-top: .6rem; padding-top: .55rem; border-top: 1px solid var(--mpa-hairline); font-size: var(--mpa-text-sm); font-weight: 600; }
.mpa-ev-k { font-weight: 500; color: var(--mpa-muted); text-transform: uppercase; letter-spacing: .04em; font-size: var(--mpa-text-xs); margin-right: .15rem; }

/* MOBILE-UI-POLISH-V1 (correction): a very long venue name in the Timetable venue-filter pills wraps
   inside the pill instead of running past the viewport. */
.mpa-venue-pill { white-space: normal; max-width: 100%; min-width: 0; overflow-wrap: anywhere; text-align: left; }
@media (max-width: 767.98px) {
  /* per-draft action buttons: keep each label on one line at 375px (pairs stay equal-height) */
  .mpa-rc-actions > .btn { white-space: nowrap; padding-left: .5rem; padding-right: .5rem; }
}
@media (max-width: 767.98px) {
  /* equal tracks regardless of label length, so paired buttons are always identical widths */
  .mpa-rc-actions, .mpa-rc-toolbar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mpa-rc-actions > .btn { min-width: 0; padding-left: .35rem; padding-right: .35rem; }
}
/* Events card footer: ticket + revenue read as one left-aligned run (never floated to the right edge). */
.mpa-ev-foot { justify-content: flex-start; }
.mpa-ev-foot > .mpa-link, .mpa-ev-foot > span { margin-left: 0 !important; }

/* ── MOBILE-UI-POLISH-V1 close-out (30 Sep 2026) ─────────────────────────────── */
/* Attendance session header: one metadata block under the title — same size, same left
   edge on every line; venue and teacher share a line and wrap as whole items. */
.mpa-att-meta { display: flex; flex-direction: column; gap: .2rem; font-size: var(--mpa-text-sm); color: var(--mpa-muted); line-height: 1.4; }
.mpa-att-meta-when { display: flex; flex-wrap: wrap; align-items: center; gap: .2rem .6rem; font-size: var(--mpa-text-base); }
.mpa-att-meta-when > .mpa-chip { flex: 0 0 auto; }
.mpa-att-meta-where { display: flex; flex-wrap: wrap; column-gap: .4rem; }
/* the separator trails the previous item, so a wrapped line never STARTS with "·" */
.mpa-att-meta-where > span:not(:last-child)::after { content: " ·"; }

/* Shared auto-fill card grid (close-out, 30 Sep 2026): cards take a content-sized track (the
   .help-cat-grid auto-fill convention; 22rem ≈ 312px at this app's root size), auto-FILL keeps a lone card one track wide instead of
   stretching it across the page, and min(…,100%) keeps a single full-width column on any phone. */
.mpa-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(22rem, 100%), 1fr)); gap: 1rem; }
.mpa-card-grid > * { min-width: 0; }

/* THE Save standard (close-out, 30 Sep 2026 — PROJECT_CONTEXT §9.3.1 rule 12): a section-level
   save reads just "Save" (no floppy icon) and is content-sized on desktop, full content width on
   phones so it matches the full-width field it sits under. */
@media (max-width: 767.98px) { .btn.mpa-btn-save { width: 100%; flex: 1 1 100%; } }

/* Edit Event (close-out, 30 Sep 2026) ───────────────────────────────────────── */
/* Secondary "how it works" copy, collapsed by default (plain <details>, keyboard-operable). */
.mpa-help-details { font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
.mpa-help-details > summary { cursor: pointer; font-weight: 600; color: var(--mpa-ink-brand); }
.mpa-help-details > div { margin-top: .4rem; }
/* URL slug: compact combined control on desktop; on phones the full address is a readable line
   above a full-width field, with a full-width Generate button beneath it. */
.mpa-slug-preview { font-size: var(--mpa-text-sm); color: var(--mpa-muted); overflow-wrap: anywhere; margin-bottom: .35rem; }
@media (max-width: 575.98px) {
  .input-group.mpa-slug-group { display: grid; grid-template-columns: 1fr; gap: .45rem; }
  .input-group.mpa-slug-group > .input-group-text { display: none; }
  .input-group.mpa-slug-group > .form-control,
  .input-group.mpa-slug-group > .btn { width: 100%; margin-left: 0 !important; border-radius: var(--bs-border-radius) !important; }
}
/* Form action group: a compact row on desktop; on phones a deliberate full-width stack in
   priority order (primary first), never a cramped wrapping row. */
.mpa-form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
@media (max-width: 575.98px) {
  .mpa-form-actions { flex-direction: column; align-items: stretch; }
  .mpa-form-actions > .btn { width: 100%; }
}

/* ── MOBILE-UI-POLISH-V1 close-out 2 (30 Sep 2026) ────────────────────────────── */
/* Detail-head width reclaim (phones only; desktop untouched): only the eyebrow ("ATTENDANCE",
   "REPORT CARDS") sits beside the hamburger and logo; the title and metadata below use the FULL
   content width instead of staying indented under the hamburger. The head-row / head-text wrappers
   dissolve into the grid (display: contents). Reusable on any .mpa-detail-head page. */
@media (max-width: 767.98px) {
  .mpa-detail-head.mpa-head-reclaim { grid-template-columns: auto minmax(0, 1fr) auto; }
  .mpa-head-reclaim > .mpa-detail-head-row,
  .mpa-head-reclaim .mpa-detail-head-text { display: contents; }
  .mpa-head-reclaim .mpa-menu-btn { grid-column: 1; grid-row: 1; align-self: center; }
  .mpa-head-reclaim .mpa-detail-module { grid-column: 2; grid-row: 1; align-self: center; margin: 0; }
  .mpa-head-reclaim > .mpa-home-logo { grid-column: 3; grid-row: 1; align-self: center; }
  .mpa-head-reclaim .mpa-detail-name { grid-column: 1 / -1; grid-row: 2; margin-top: .1rem; }
  .mpa-head-reclaim .mpa-att-meta,
  .mpa-head-reclaim .mpa-detail-subtitle { grid-column: 1 / -1; grid-row: 3; }
}
/* Roll-call bulk actions on phones: a 50/50 pair, equal height, on the roll-call content width. */
.mpa-bulk-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; }
.mpa-bulk-pair > .btn { width: 100%; min-height: 2.25rem; }
@media (max-width: 767.98px) {
  .mpa-head-reclaim .mpa-detail-actions > .form-select { width: 100% !important; }
}

/* Timetable header (close-out 2): one non-wrapping desktop row so the logo is never orphaned; the
   bulk import/export group may wrap INSIDE itself when space is tight; the term select is bounded. */
.mpa-tt-bulk { flex-wrap: wrap; gap: .5rem; align-items: center; }
.mpa-tt-actions { display: flex; flex-direction: column; align-items: flex-end; gap: .2rem; }
.mpa-tt-range { margin: 0; text-align: right; white-space: nowrap; }
@media (max-width: 767.98px) { .mpa-tt-actions { align-items: stretch; } }
@media (min-width: 992px) {
  .mpa-page-head.mpa-tt-head { flex-wrap: nowrap; }
  .mpa-tt-head .mpa-page-title { white-space: nowrap; }
  .mpa-tt-head > .mpa-tt-bulk { flex: 0 1 auto; min-width: 0; }
  .mpa-tt-head > .mpa-term-block { flex: 0 0 auto; }
  .mpa-tt-head > .mpa-home-logo { flex: 0 0 auto; }
  .mpa-tt-head .mpa-tt-term { max-width: 260px !important; }
}
@media (min-width: 992px) {
  /* Empty term only (no week controls to carry the range): the range sits inline before the logo
     rather than forcing a second row (.mpa-head-secondary is a full-width line by default). */
  .mpa-tt-head > .mpa-head-secondary { flex: 0 0 auto; width: auto; order: 1; margin: 0; white-space: nowrap; }
  .mpa-tt-head > .mpa-home-logo { order: 2; }
}
@media (min-width: 992px) { .mpa-tt-head > .mpa-tt-primary { flex-wrap: nowrap; flex: 0 0 auto; } .mpa-tt-primary > .btn { white-space: nowrap; } }

/* ══ MOBILE UI CONSISTENCY — FINAL SYSTEM PASS (30 Sep 2026) ═════════════════════════════
   THE mobile patterns, defined once:
   A. TOP-LEVEL page   .mpa-page-head     [hamburger] TITLE [logo]; a single primary action is a
                                          full-width row beneath (.mpa-head-actions).
   B. DETAIL / edit    .mpa-detail-head   [hamburger] EYEBROW [logo], then the ENTITY TITLE and its
                                          metadata on the FULL content width (this block).
   C. ACTIONS          .mpa-detail-actions  an equal 2-up grid (a lone/odd last action spans the row);
                                          destructive styling is left to the button classes.
   D. DETAIL CARDS     dl.mpa-dl          label | value on a content-sized label column, values wrap
                                          anywhere — never a squeezed 42% column.
   E. OPERATIONAL LISTS  phones get cards, not a horizontally-scrolling table (.mpa-cp-*).        */
@media (max-width: 767.98px) {
  /* B: every .mpa-detail-head except the bespoke invoice head reclaims the width below the hamburger */
  .mpa-detail-head { grid-template-columns: auto minmax(0, 1fr) auto; }
  .mpa-detail-head > .mpa-detail-head-row,
  .mpa-detail-head .mpa-detail-head-text { display: contents; }
  .mpa-detail-head .mpa-menu-btn { grid-column: 1; grid-row: 1; align-self: center; }
  .mpa-detail-head .mpa-detail-module { grid-column: 2; grid-row: 1; align-self: center; margin: 0; }
  .mpa-detail-head > .mpa-home-logo { grid-column: 3; grid-row: 1; align-self: center; }
  .mpa-detail-head .mpa-detail-name { grid-column: 1 / -1; grid-row: 2; margin-top: .1rem; }
  .mpa-detail-head .mpa-att-meta,
  .mpa-detail-head .mpa-detail-subtitle { grid-column: 1 / -1; grid-row: 3; }
  /* C: paired actions */
  .mpa-detail-head .mpa-detail-actions {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; width: 100%;
  }
  .mpa-detail-head .mpa-detail-actions > form { display: contents; }
  .mpa-detail-head .mpa-detail-actions > .btn,
  .mpa-detail-head .mpa-detail-actions > form > .btn { width: 100%; min-width: 0; }
  .mpa-detail-head .mpa-detail-actions > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  /* An action whose label can't fit half a phone row opts into the whole row
     with the existing .mpa-action-full (as in .mpa-setup-actions); dense flow
     lets the shorter actions pair up on the row above it (Curriculum's
     "Replace with newer version", 14 Oct 2026). */
  .mpa-detail-head .mpa-detail-actions > .mpa-action-full { grid-column: 1 / -1; }
  .mpa-detail-head .mpa-detail-actions:has(> .mpa-action-full) { grid-auto-flow: row dense; }
  /* With one action on its own row the others pair up, so the "lone odd
     last action spans" rule above no longer applies. */
  .mpa-detail-head .mpa-detail-actions:has(> .mpa-action-full) > :last-child:nth-child(odd):not(.mpa-action-full) { grid-column: auto; }
}
/* D: detail-card label/value list (phones) */
@media (max-width: 575.98px) {
  dl.row.mpa-dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: 1rem; row-gap: 0; margin: 0; }
  dl.row.mpa-dl > dt, dl.row.mpa-dl > dd { width: auto; max-width: none; flex: none; margin: 0; padding: .6rem 0; border-top: 1px solid var(--mpa-hairline); }
  dl.row.mpa-dl > dt:first-of-type, dl.row.mpa-dl > dt:first-of-type + dd { border-top: 0; padding-top: 0; }
  dl.row.mpa-dl > dt { white-space: nowrap; font-weight: 600; color: var(--mpa-muted); font-size: var(--mpa-text-sm); align-self: start; padding-top: .6rem; }
  dl.row.mpa-dl > dd { min-width: 0; overflow-wrap: anywhere; }
}
/* E: Class Plans phone cards */
.mpa-cp-list { padding: 0 .9rem .9rem; display: flex; flex-direction: column; gap: .7rem; }
.mpa-cp-card { border: 1px solid var(--mpa-hairline); border-radius: var(--mpa-radius-sm, .6rem); padding: .8rem .9rem; background: var(--mpa-surface); }
.mpa-cp-top { display: flex; align-items: flex-start; gap: .5rem; flex-wrap: wrap; }
.mpa-cp-name { font-weight: 700; color: var(--mpa-ink-brand); text-decoration: none; overflow-wrap: anywhere; min-width: 0; }
.mpa-cp-meta { display: flex; flex-wrap: wrap; align-items: center; gap: .15rem .7rem; margin: .3rem 0 .65rem; font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
.mpa-cp-action { display: block; width: 100%; }
.mpa-cp-reason { margin-top: .35rem; font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
/* Timetable desktop density (band: week label · term range) */
@media (min-width: 992px) {
  .mpa-weeknav.mpa-tt-weeknav { max-width: none; margin-bottom: .6rem !important; }
  .mpa-tt-weeknav .mpa-weeknav-controls { margin-bottom: 0 !important; }
  .mpa-tt-controls-row { margin-bottom: .6rem !important; }
}
.mpa-tt-actions { align-items: flex-end; }

/* top-level pages that group hamburger + title in .mpa-head-row (Staff, Report Cards) use the SAME
   hamburger→title gap as the grid pattern (.7rem), so the title starts on the same pixel everywhere */
@media (max-width: 767.98px) { .mpa-page-head > .mpa-head-row { column-gap: .7rem; gap: .7rem; } }
@media (max-width: 767.98px) { .mpa-page-head > .mpa-head-row > .mpa-menu-btn { margin-right: 0 !important; } }

/* ══ FINAL SYSTEM ENFORCEMENT (30 Sep 2026) ══════════════════════════════════════════════════════
   ONE canonical detail header (partials/_detail_header.html · detail_head): every .mpa-detail-head —
   invoices included — is [hamburger] EYEBROW [logo] then the record title/metadata/actions on the full
   content width. Invoice: the secondary metadata is a disclosure that sits BELOW the status chips (collapsed by default).       */
.mpa-inv-details { margin: 0; }
.mpa-inv-details > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: .4rem; font-size: var(--mpa-text-sm); font-weight: 600; color: var(--mpa-ink-brand); }
.mpa-inv-details > summary::-webkit-details-marker { display: none; }
.mpa-inv-details[open] > summary .mpa-collapse-chevron { transform: rotate(180deg); }
.mpa-inv-details-body { margin-top: .35rem; }
/* 1 Oct 2026: the disclosure is the compact default at EVERY width (collapsed, summary always visible). */
@media (max-width: 767.98px) {
  /* the status chips precede the disclosure in the markup itself (1 Oct 2026) — no CSS re-ordering */
  .mpa-invoice-head .mpa-detail-meta { display: flex; flex-direction: column; grid-column: 1 / -1; }
}

/* ══ DESIGN RULES — SYSTEMIC DENSITY / TYPOGRAPHY PASS (1 Oct 2026) ══════════════════════════════
   Small, reusable rules so a page built on the shared classes looks right without per-page margin
   fixes (PROJECT_CONTEXT §9.3.1 rule 14). Everything below is presentation only. Pinned by
   qa/test_mobile_ui_polish_v1.py § "DESIGN RULES" (geometry / computed-style assertions).

   R1 SPACING SCALE   one scale, four steps. Phones use the compact values.
        --mpa-space-tight  .35rem  inside one logical group (label → value, chips → their disclosure)
        --mpa-space-group  .5rem   header rows (eyebrow → title → status → actions), callout internals
        --mpa-space-block  1rem / phone .75rem   block → block (header → callout → card → card, field → field)
        --mpa-space-section 1.35rem / phone 1rem  major section → next section
      On phones the Bootstrap page-level utilities (mb/mt/my -3/-4/-5, row gutters g-3/g-4) inside
      the app's <main> resolve to these tokens, so a template's "mb-4" means "the block gap", not 24px.
   R2 DETAIL HEADER   the canonical .mpa-detail-head's rows are one group: --mpa-space-group apart.
   R3 CALLOUT + ACTION  .mpa-callout-action: copy and action side by side where there is room; on
      phones the copy keeps the full width and the action is its own full-width row beneath it.
   R4 EXPLANATORY CALLOUTS  phones drop the DECORATIVE leading info/lightbulb glyph of an
      info/light/secondary/primary .alert so the text starts on the callout's content edge.
      Warning/danger/success alerts and every non-info glyph keep their icons.
   R5 DETAIL LISTS    dl.mpa-dl / dl.mpa-detail-dl: ONE label treatment, ONE value treatment at every
      width; links and inline spans inside a value inherit its size (a link is a colour, not a scale).
   R6 CARD HEADERS    a card title never shrinks below its longest word, so a count chip or trailing
      link can sit beside it but never over it; a header BUTTON's icon keeps the button's own colour.
   R7 STANDALONE ACTIONS  .btn.mpa-btn-save (rule 12): the lone primary action of a form section is
      content-sized on desktop and full content width on phones.
   R8 AFFIXED INPUTS  .mpa-input-affix: an input-group whose prefix is long text (a URL) stacks the
      prefix above the field on phones instead of crushing the field.                               */
:root {
  --mpa-space-tight: .35rem;
  --mpa-space-group: .5rem;
}
@media (max-width: 767.98px) {
  :root {
    --mpa-space-block: .75rem;
    --mpa-space-section: 1rem;
    --mpa-space-label: .45rem;
  }
  /* R1 — page-level utilities resolve to the scale (phones, app content only) */
  main.main-content .mb-3, main.main-content .mb-4 { margin-bottom: var(--mpa-space-block) !important; }
  main.main-content .mt-3, main.main-content .mt-4 { margin-top: var(--mpa-space-block) !important; }
  main.main-content .my-3, main.main-content .my-4 { margin-top: var(--mpa-space-block) !important; margin-bottom: var(--mpa-space-block) !important; }
  main.main-content .mb-5 { margin-bottom: var(--mpa-space-section) !important; }
  main.main-content .mt-5 { margin-top: var(--mpa-space-section) !important; }
  main.main-content .my-5 { margin-top: var(--mpa-space-section) !important; margin-bottom: var(--mpa-space-section) !important; }
  main.main-content .row.g-3, main.main-content .row.g-4, main.main-content .row.gy-3, main.main-content .row.gy-4 { --bs-gutter-y: var(--mpa-space-block); }
  main.main-content .alert { margin-bottom: var(--mpa-space-block); }

  /* R2 — detail header: eyebrow row → title → status/metadata → actions are one group */
  .mpa-detail-head { row-gap: var(--mpa-space-group); margin-bottom: var(--mpa-space-block); }
  .mpa-detail-head .mpa-detail-name { margin-top: 0; margin-bottom: 0 !important; }
  .mpa-detail-head .mpa-detail-meta > .mt-1 { margin-top: 0 !important; }
  .mpa-detail-head .mpa-detail-meta { row-gap: var(--mpa-space-tight); }

  /* R4 — explanatory callouts: compact inset, body size, no decorative leading glyph */
  main.main-content .alert { font-size: var(--mpa-text-md); padding-top: .6rem; padding-bottom: .6rem; padding-left: .8rem; }
  main.main-content .alert:not(.alert-dismissible) { padding-right: .8rem; }
  .alert:is(.alert-info, .alert-light, .alert-secondary, .alert-primary) > .bi:is(.bi-info-circle, .bi-info-circle-fill, .bi-info-square, .bi-info-square-fill, .bi-info-lg, .bi-lightbulb, .bi-lightbulb-fill):first-child,
  .alert:is(.alert-info, .alert-light, .alert-secondary, .alert-primary) > :first-child > .bi:is(.bi-info-circle, .bi-info-circle-fill, .bi-info-square, .bi-info-square-fill, .bi-info-lg, .bi-lightbulb, .bi-lightbulb-fill):first-child {
    display: none;
  }
}

/* R2 (all widths) — the invoice header's status-chip row sits directly under the name: the header's own row gap
   is the separation, not an extra .mt-1 on top of it */
.mpa-invoice-head .mpa-detail-meta > .mt-1 { margin-top: 0 !important; }

/* R3 — callout with its own action */
.mpa-callout-action { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem 1rem; }
.mpa-callout-action > :first-child { flex: 1 1 auto; min-width: 0; }
.mpa-callout-action > .mpa-callout-action-btn { flex: 0 0 auto; margin: 0; }
@media (max-width: 767.98px) {
  .mpa-callout-action { flex-direction: column; align-items: stretch; gap: var(--mpa-space-group); }
  .mpa-callout-action > .mpa-callout-action-btn,
  .mpa-callout-action > .mpa-callout-action-btn > .btn { width: 100%; }
  .mpa-callout-action > .mpa-callout-action-btn > .btn { white-space: normal; }
}

/* R5 — detail lists: one label role, one value role */
:is(dl.mpa-dl, dl.mpa-detail-dl) { --mpa-dl-value-size: var(--mpa-text-md); --mpa-dl-value-lh: 1.45; }
:is(dl.mpa-dl, dl.mpa-detail-dl) > dt {
  font-size: var(--mpa-text-sm); font-weight: 600; line-height: 1.4; color: var(--mpa-muted);
}
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd {
  font-size: var(--mpa-dl-value-size); font-weight: 500; line-height: var(--mpa-dl-value-lh); color: var(--mpa-ink);
}
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd > :is(a, span:not(.mpa-chip), small, strong):not(.btn) {
  font-size: inherit; line-height: inherit;
}
/* block-level helper copy under a value (div.small) keeps the helper/meta size; a chip inside a value may
   wrap rather than push past the value column */
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd .mpa-chip { max-width: 100%; white-space: normal; overflow-wrap: anywhere; text-align: left; }
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd a:not(.btn) { font-weight: inherit; }
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd .mpa-dl-emph { font-weight: 700; }
/* a long unbroken value (email, URL, id) breaks only where it must, never forcing the card wider */
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd, :is(dl.mpa-dl, dl.mpa-detail-dl) > dd a { overflow-wrap: anywhere; word-break: normal; }

/* R7b — two peer actions (.mpa-action-pair): the SAME equal 2-up grid the detail header uses, for a
   card footer / section with two peer buttons; a lone or odd last action, or a note, spans the row. */
@media (max-width: 767.98px) {
  .mpa-action-pair { display: grid !important; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .5rem; }
  .mpa-action-pair > form { display: contents; }
  .mpa-action-pair > .btn, .mpa-action-pair > form > .btn { width: 100%; min-width: 0; }
  .mpa-action-pair > :last-child:nth-child(odd), .mpa-action-pair > :not(.btn):not(form) { grid-column: 1 / -1; }
}

/* Reorderable settings list (.mpa-reorder-table: drag handle | name | usage | actions) — phones: one compact
   row per item: handle spans both lines, name then its usage beneath, actions at the right. */
@media (max-width: 575.98px) {
  .mpa-reorder-table thead { display: none; }
  .mpa-reorder-table, .mpa-reorder-table tbody { display: block; width: 100%; }
  .mpa-reorder-table tr {
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; column-gap: .6rem; row-gap: .1rem;
    align-items: center; padding: .55rem 0; border-top: 1px solid var(--mpa-hairline);
  }
  .mpa-reorder-table tbody tr:first-child { border-top: 0; }
  .mpa-reorder-table td { display: block; border: 0 !important; padding: 0 !important; min-width: 0 !important; max-width: none !important; background: transparent !important; }
  .mpa-reorder-table td:nth-child(1) { grid-column: 1; grid-row: 1 / span 2; }
  .mpa-reorder-table td:nth-child(2) { grid-column: 2; grid-row: 1; }
  .mpa-reorder-table td:nth-child(3) { grid-column: 2; grid-row: 2; }
  .mpa-reorder-table td:nth-child(4) { grid-column: 3; grid-row: 1 / span 2; white-space: normal !important; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .35rem; }
}

/* R6 — card headers */
.mpa-card-head > .mpa-card-title { min-width: min-content; }
.card > .card-header .btn .bi:first-child { color: inherit; font-size: inherit; }

/* R8 — affixed inputs with a long text prefix */
@media (max-width: 575.98px) {
  .input-group.mpa-input-affix { flex-wrap: wrap; }
  .input-group.mpa-input-affix > .input-group-text {
    flex: 1 1 100%; white-space: normal; overflow-wrap: anywhere; justify-content: flex-start;
    border-radius: var(--mpa-radius-sm) var(--mpa-radius-sm) 0 0 !important; border-bottom: 0;
  }
  .input-group.mpa-input-affix > .form-control {
    flex: 1 1 100%; width: 100%; margin-left: 0 !important;
    border-radius: 0 0 var(--mpa-radius-sm) var(--mpa-radius-sm) !important;
  }
}

/* Feature Access (Settings): one card, one stacked record per feature on phones (.mpa-table-stack);
   the description under each feature title is helper copy, not part of the bold title line. */
@media (min-width: 992px) {
  .mpa-feature-access-table > * > tr > :first-child { padding-left: 1.25rem; }
  .mpa-feature-access-table > * > tr > :last-child { padding-right: 1.25rem; }
}
.mpa-feature-access-table td:first-child .mpa-fa-desc { font-weight: 400; font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
@media (max-width: 991.98px) {
  .mpa-feature-access-table.mpa-table-stack td:first-child { padding-bottom: .2rem !important; }
  .mpa-feature-access-table.mpa-table-stack td:not(:first-child) { align-items: center; }
  .mpa-feature-access-table .form-check.form-switch { justify-content: flex-end !important; margin: 0; min-height: 0; }
}

/* ══════════════════════════════════════════════════════════════════════
   PARENT PORTAL — invoice list cards + invoice line alignment
   (FreeAgent V1 demo polish, 1 Oct 2026)
   The list was one hairline row stretched across the whole viewport: now a
   width-capped card of two-line rows — pupil over what/when on the left,
   amount over status on the right; each meta piece wraps whole, and on
   phones they stack (no "Autumn Term 2026 · 2026-10-01" mid-phrase wrap). */
.mpa-pinv-list { max-width: 46rem; }
/* 24 Oct 2026 (Rory): each invoice is its own card, two equal columns from 768px (one on phones);
   more invoices wrap into new rows and an odd last card keeps the column width. */
.mpa-pinv-list.mpa-pinv-cards { max-width: none; display: grid; grid-template-columns: minmax(0, 1fr);
  gap: var(--mpa-space-block); }
@media (min-width: 768px) { .mpa-pinv-list.mpa-pinv-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.mpa-card.mpa-pinv-card { display: flex; min-width: 0; color: var(--mpa-ink); text-decoration: none;
  transition: border-color .15s ease; }
.mpa-card.mpa-pinv-card:hover { border-color: var(--mpa-action); color: var(--mpa-ink); }
.mpa-card.mpa-pinv-card:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: 2px; }
.mpa-pinv-card:is(:hover, :focus-visible) .mpa-row-chev { opacity: 1; transform: none; }
/* In a card the term / issued / due pieces stack (as on phones): in half a row a long term name made the
   line wrap with its "·" separator stranded at the start of the next line. */
.mpa-pinv-card .mpa-pinv-meta { flex-direction: column; }
.mpa-pinv-card .mpa-pinv-meta > span + span::before { content: none; }
:is(.mpa-row, .mpa-card).mpa-pinv { align-items: center; gap: 1rem; padding: .9rem 1.25rem; }
.mpa-pinv-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .2rem; }
.mpa-pinv-name { font-weight: 700; font-size: var(--mpa-text-base); line-height: 1.3; overflow-wrap: anywhere; }
.mpa-pinv-meta {
  display: flex; flex-wrap: wrap; column-gap: .45rem; row-gap: .1rem;
  color: var(--mpa-muted); font-size: var(--mpa-text-sm); line-height: 1.4;
}
.mpa-pinv-meta > span { white-space: nowrap; }
/* Density pass (9 Oct 2026, P0): the invoice's title (term / month name) may be longer than the row — it
   wraps inside its column instead of running under the amount/status chip and off the card on phones. */
.mpa-pinv-meta > span:first-child { white-space: normal; overflow-wrap: anywhere; }
.mpa-pinv-meta > span + span::before { content: "·"; margin-right: .45rem; color: var(--mpa-muted); }
.mpa-pinv-side { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: .35rem; }
.mpa-pinv-amount {
  font-weight: 700; font-size: var(--mpa-text-lg); line-height: 1.2; color: var(--mpa-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
@media (max-width: 575.98px) {
  :is(.mpa-row, .mpa-card).mpa-pinv { align-items: flex-start; gap: .75rem; padding: .85rem 1rem; }
  .mpa-pinv-meta { flex-direction: column; }
  .mpa-pinv-meta > span + span::before { content: none; }
  :is(.mpa-row, .mpa-card).mpa-pinv .mpa-row-chev { align-self: center; }
}
/* Invoice detail lines: notes INSIDE a row (session detail "10 × £7.00",
   "Subtotal before discounts" and its £) sit in the row's own columns —
   not the standalone .mpa-rows .mpa-stat-note inset. */
.mpa-pinv-lines .mpa-row .mpa-stat-note { padding: 0; margin: 0; }

/* Dashboard setup card actions (demo polish, 1 Oct 2026): on phones the
   actions are full-width buttons, stacked — "Continue setup" over "Dismiss",
   or a single full-width "Dismiss" once setup is complete. */
@media (max-width: 575.98px) {
  [data-setup-banner] > .mpa-setup-actions { width: 100%; flex-direction: column; align-items: stretch !important; }
  [data-setup-banner] > .mpa-setup-actions > .btn,
  [data-setup-banner] > .mpa-setup-actions > form > .btn { width: 100%; }
}

/* Mark-as-paid amount choice (demo corrective, 1 Oct 2026): the actions start
   exactly under the warning's own text (no extra inset), wrap as whole
   buttons on desktop, and become full-width buttons on phones. */
.mpa-fee-choice-body { flex: 1 1 auto; min-width: 0; }
.mpa-fee-choice-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.mpa-fee-choice-actions > .btn-link { padding-left: .5rem; padding-right: .5rem; color: var(--mpa-ink-brand); }
/* Final acceptance polish (1 Oct 2026): two equally legitimate amounts —
   readable brand-outline buttons (brand fill on hover), the suggested one
   with only a faint tint; an explicit focus ring on all three (the alert's
   own background can swallow Bootstrap's soft focus shadow). */
.mpa-fee-choice-actions .btn-outline-primary { background: var(--mpa-surface); }
.mpa-fee-choice-actions .btn-outline-primary.is-suggested { background: rgba(var(--mpa-action-rgb), .07); }
.mpa-fee-choice-actions .btn-outline-primary:hover,
.mpa-fee-choice-actions .btn-outline-primary:active { background: var(--mpa-action); color: var(--mpa-on-action); }
.mpa-fee-choice-actions .btn:focus-visible { outline: 2px solid var(--mpa-ink-brand); outline-offset: 2px; }
.mpa-fee-choice-actions > .btn-link:hover,
.mpa-fee-choice-actions > .btn-link:focus-visible { background: rgba(var(--mpa-action-rgb), .07); }
@media (max-width: 575.98px) {
  .mpa-fee-choice-actions { flex-direction: column; align-items: stretch; }
  .mpa-fee-choice-actions > form > .btn { width: 100%; white-space: normal; }
}

/* Invoice header status line. The demo polish (1 Oct 2026) pulled the first
   status chip left by its own side padding (-.6rem), so its TEXT lined up
   with the heading. Retired 8 Oct 2026 (Invoice Document V1 manual
   acceptance, Rory):
   - the more strongly filled chips (Paid, Sent to parent) clearly read as an
     edge, so the pill hung outside the content edge — left of the title,
     "Invoice details", the full-width phone actions and the callouts below;
   - that broke the 30 Sep canonical detail-header contract ("metadata and
     actions start on the same content edge", pinned by
     qa/test_mobile_ui_polish_v1.py ENF).
   The chip now sits on the header's content edge like every other header
   element. No override is needed — the shared rules already place it there. */

/* ══ SETTINGS UI — DENSITY + CONSISTENCY STANDARD (2 Oct 2026) ═══════════════════════════════════
   One standard for the Settings area (School / Billing / Integrations & Advanced and every sub-tab).
   Scope: every Settings page renders settings/_nav.html, so "main containing .settings-group-nav" is the
   area — no other page is affected. Phones keep the R1 phone scale; the density steps below apply from
   768px, the two-column layouts from 900px (so a laptop on 125–150% Windows scaling — CSS width ~900–1000
   — still gets them).

   SPACING (one hierarchy, rem — 13px at >= 992px, 16px below)
     --set-gap         .75rem  card → card · callout → card · last card → its Save row (the row belongs
                               to the cards above it) · column gap in two-column layouts
     --set-pad-top     .6rem   card header top          --set-pad-bottom .75rem  card body bottom
     --set-label-gap   .25rem  label → control          --set-help-gap   .25rem  control → its help text
     --set-group-gap   .65rem  form group → next group inside a card
     --set-section-gap 1.35rem a form's Save row → the next independent section (= --mpa-space-section)
     --set-measure     75ch    longest line of explanatory prose inside a card
   CLASSES (semantic; templates opt in)
     .set-actions      a form's Save row (directly under the last card it saves)
     .set-footnote     a note / metadata line under a Save row
     .set-pair         two related cards side by side from 900px (one column below); columns flow
                       independently — no forced equal heights
     .set-columns      two INDEPENDENT columns from 900px (.set-col-main / .set-col-side; items .set-col-item
                       with --set-order giving the one-column order below 900px). .set-columns-even = equal
                       columns (default 7:5). .set-branding is Branding's instance (items .set-b-item).
     .set-upload       compact upload row: thumbnail | status + hint + file control
     .set-empty        a list page's empty state: the blue callout BELOW the explanatory/action card
     .set-status-row   a label + status chip on one centred row
     .set-num / .set-num-pct / .set-num-money / .input-group.set-num-group   compact numeric fields
                       (global, safe: class-based) — the FIELD is compact, the control keeps its full
                       height and an addon (days / % / £) stays attached                              */
main.main-content:has(.settings-group-nav) {
  --set-gap: .75rem;
  --set-pad-top: .6rem;
  --set-pad-bottom: .75rem;
  --set-label-gap: .25rem;
  --set-help-gap: .25rem;
  --set-group-gap: .65rem;
  --set-section-gap: var(--mpa-space-section);
  --set-measure: 75ch;
}

/* Compact numeric fields: whole numbers (days, counts, thresholds: 1–3 digits) · percentages (up to
   "100.00") · money (up to £99.99). Below the 992px desktop scale (16px root) they keep a tap-friendly
   width — a whole-number field is never narrower than ~96px there (UI-CONSISTENCY-1B F4: an obvious,
   real field on phones/tablets); at the desktop scale they tighten to fit the data. */
.form-control.set-num { width: 6rem; flex: 0 0 auto; text-align: center; }
.form-control.set-num-pct { width: 6.5rem; flex: 0 0 auto; }
.form-control.set-num-money { width: 7rem; flex: 0 0 auto; }
@media (min-width: 992px) {
  .form-control.set-num { width: 4.75rem; }
  .form-control.set-num-pct { width: 6rem; }
  .form-control.set-num-money { width: 6.5rem; }
}
.input-group.set-num-group { width: fit-content; max-width: 100%; flex-wrap: nowrap; }
.input-group.set-num-group > .form-control:is(.set-num, .set-num-pct, .set-num-money) { flex: 0 0 auto; }

/* Layout primitives (all widths) */
main.main-content:has(.settings-group-nav) .set-pair {
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--set-gap); align-items: start;
  margin-bottom: var(--set-gap);
}
main.main-content:has(.settings-group-nav) .set-pair > * { margin: 0; min-width: 0; }
main.main-content:has(.settings-group-nav) .set-actions { margin-top: 0; margin-bottom: 0; }
main.main-content:has(.settings-group-nav) .set-footnote { margin: var(--set-group-gap) 0 0; }
main.main-content:has(.settings-group-nav) .set-empty { margin-top: 0; }
main.main-content:has(.settings-group-nav) .set-status-row { display: flex; align-items: center; flex-wrap: wrap; gap: .4rem; }
main.main-content:has(.settings-group-nav) .set-status-row > .mpa-chip { margin: 0; }
/* .set-actionbar (density pass, 9 Oct 2026): the Save row of SEVERAL cards laid out side by side in one
   grid/form. It is the grid's last item and spans every column, on a quiet surface joined to the cards
   (same line/radius), so the action reads as belonging to all of them: [card][card] / [Save · note].
   One step (--set-gap) above it, like any card → card. Use .set-actions for a Save under ONE card. */
.set-actionbar {
  grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .9rem; margin: 0;
  padding: .6rem 1.25rem; background: var(--mpa-surface); border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius); box-shadow: var(--mpa-shadow);
}
@media (max-width: 767.98px) { .set-actionbar { padding: .6rem 1rem; } }

/* Two independent columns (Branding, Report Cards): one column below 900px in a logical order (--set-order;
   the column wrappers use display:contents there) → two independent columns from 900px */
main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) { display: flex; flex-direction: column; gap: var(--set-gap); }
main.main-content:has(.settings-group-nav) .set-columns { margin-bottom: var(--set-gap); }
main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) > .set-col { display: contents; }
main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) :is(.set-b-item, .set-col-item) { order: var(--set-order, 0); margin: 0; min-width: 0; }
main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) :is(.set-b-item, .set-col-item) > .card { margin: 0; }
@media (min-width: 900px) {
  main.main-content:has(.settings-group-nav) .set-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) { flex-direction: row; align-items: flex-start; }
  main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) > .set-col { display: flex; flex-direction: column; gap: var(--set-gap); min-width: 0; }
  main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) > .set-col-main { flex: 7 1 0; }
  main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) > .set-col-side { flex: 5 1 0; }
  main.main-content:has(.settings-group-nav) .set-columns.set-columns-even > :is(.set-col-main, .set-col-side) { flex: 1 1 0; }
  main.main-content:has(.settings-group-nav) :is(.set-branding, .set-columns) :is(.set-b-item, .set-col-item) { order: 0; }
}

/* Compact upload card (Branding logo / favicon / home-screen icon): thumbnail + status/hint on one row,
   the file control and its Upload button full width beneath (never squeezed beside the thumbnail) */
.set-upload { display: flex; align-items: flex-start; gap: .75rem; }
.set-upload-thumb { flex: none; width: 48px; height: 48px; object-fit: contain; border-radius: 8px;
  border: 1px solid var(--mpa-line); background: #f0f0f0; padding: 3px; }
.set-upload-thumb-wide { width: auto; max-width: 7rem; }          /* a logo keeps its own aspect ratio */
.set-upload-thumb-app { border-radius: 12px; }                     /* a home-screen icon's rounded corners */
.set-upload-body { flex: 1 1 auto; min-width: 0; }
.set-upload-form { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-top: .6rem; }
.set-upload-form > input[type=file] { flex: 1 1 12rem; min-width: 0; max-width: 22rem; }

@media (min-width: 768px) {
  /* card → card: one step, whatever utility a template used */
  main.main-content:has(.settings-group-nav) :is(.card, .mpa-card):is(.mb-3, .mb-4) { margin-bottom: var(--set-gap) !important; }
  /* page-level callouts: compact inset, the same step below */
  main.main-content:has(.settings-group-nav) .alert:not(.card .alert, .mpa-card .alert) { padding: .6rem .9rem; }
  main.main-content:has(.settings-group-nav) .alert.alert-dismissible:not(.card .alert, .mpa-card .alert) { padding-right: 3rem; }
  main.main-content:has(.settings-group-nav) .alert:is(.mb-3, .mb-4):not(.card .alert, .mpa-card .alert) { margin-bottom: var(--set-gap) !important; }
  /* card padding: header top, body bottom (the header → body seam stays .35rem) */
  main.main-content:has(.settings-group-nav) .card {
    --bs-card-cap-padding-y: var(--set-pad-top);
    --bs-card-spacer-y: var(--set-pad-bottom);
  }
  main.main-content:has(.settings-group-nav) .mpa-card > .mpa-card-head { padding-top: calc(var(--set-pad-top) + .1rem); padding-bottom: .5rem; }
  main.main-content:has(.settings-group-nav) .mpa-card > .mpa-card-body { padding-bottom: var(--set-pad-bottom); }
  /* inside a card: label → control → help; group → group; no trailing margin; readable prose measure */
  main.main-content:has(.settings-group-nav) .card-body .form-label { margin-bottom: var(--set-label-gap); }
  main.main-content:has(.settings-group-nav) .card-body .form-text { margin-top: var(--set-help-gap); line-height: 1.4; }
  main.main-content:has(.settings-group-nav) .card-body .form-text:is(.mt-1, .mt-2, .mt-3) { margin-top: var(--set-help-gap) !important; }
  main.main-content:has(.settings-group-nav) .card-body .mb-3 { margin-bottom: var(--set-group-gap) !important; }
  main.main-content:has(.settings-group-nav) .card-body > :last-child:not(.alert) { margin-bottom: 0 !important; }
  main.main-content:has(.settings-group-nav) :is(.card-body, .mpa-card-body) :is(.form-text, p.text-muted, p.small) { max-width: var(--set-measure); }
  /* a stacked grid (columns that wrap) uses the same step between rows; side-by-side gutters unchanged */
  main.main-content:has(.settings-group-nav) .row:is(.g-3, .g-4, .gy-3, .gy-4) { --bs-gutter-y: var(--set-gap); }
}

/* Phones: a warning callout laid out as icon column + text (".alert.d-flex") drops its leading hazard
   glyph so the warning copy uses the callout's full width (R4's rule, extended to Settings warnings
   whose icon sits in its own flex column). Colour, wording and links are unchanged; danger alerts and
   inline (non-column) warning icons keep their glyphs. */
@media (max-width: 767.98px) {
  main.main-content:has(.settings-group-nav) .alert-warning.d-flex > .bi:is(.bi-exclamation-triangle, .bi-exclamation-triangle-fill, .bi-exclamation-circle, .bi-exclamation-circle-fill):first-child {
    display: none;
  }
}

/* ==========================================================================
   PUBLISHED INVOICE CORRECTIONS V1 (5 Oct 2026) — /invoices/{id}/correct
   (+ /review). One stacked row per invoice line instead of a wide table, so
   the form never scrolls sideways at 390px: description on its own line,
   money fields and the remove switch wrap beneath it on a phone and sit
   alongside it from 768px.
   ========================================================================== */
/* ── Published Invoice Corrections V1 — Correct invoice / Review correction ──
   Redesign (7 Oct 2026, Rory's manual acceptance). Replaces the first
   build's 860px-capped flex rows. Built only from the house pieces
   (.mpa-card, .mpa-chip, .mpa-alert, the --mpa-* tokens):

   DESKTOP (>= 768px): every line is one row of the SAME 5-column grid as the
   header — Line · Charge · Qty · Total · action. A fixed-amount line (extra,
   discount) puts its Amount box in the Total column (as the draft invoice
   table does) and leaves Charge/Qty empty; the admin fee shows its
   recalculated figure in Total; "Remove" and "Waive this fee" share the
   action column, so every control lines up vertically.

   PHONE (< 768px): a compact line card — name + badge with the action on
   the right, then one row of three equal columns: inputs left, the computed
   figure right (Charge | Qty | Total, or Amount | — | —). Every input is
   one predictable column wide. */
.mpa-corr-intro { line-height: 1.5; }
.mpa-corr-grid { display: flex; flex-direction: column; }
.mpa-corr-row {
  display: grid;
  /* Phone (Rory's composition): the line name has the card's full width;
     below it three EQUAL columns, so every box is one predictable width —
       class:  Charge | Qty | Total, then Remove
       extra:  Amount | Remove
       fee:    Waive this fee | … | Recalculated
       new:    Description (2 columns) | Amount */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-areas: "desc desc desc" "f1 f2 f3" "action action action";
  align-items: end;
  gap: .4rem .6rem;
  padding: .7rem 0;
  border-bottom: 1px solid var(--mpa-line);
}
.mpa-corr-row:last-child { border-bottom: 0; padding-bottom: 0; }
.mpa-corr-row:first-child:not(.mpa-corr-head) { padding-top: 0; }
.mpa-corr-c-desc {
  grid-area: desc; min-width: 0; overflow-wrap: anywhere;
  display: flex; flex-wrap: wrap; align-items: center; gap: .25rem .5rem;
  align-self: center;
}
.mpa-corr-c-desc .mpa-corr-desc-input { flex: 1 1 12rem; min-width: 0; }
/* Phone: an editable line's label sits ABOVE its box, never beside it. */
.mpa-corr-c-desc > .mpa-corr-lbl { flex: 0 0 100%; margin-bottom: 0; }
.mpa-corr-name { font-weight: 600; }
.mpa-corr-badge { flex: 0 0 auto; }
.mpa-corr-c-charge { grid-area: f1; min-width: 0; }
.mpa-corr-c-qty    { grid-area: f2; min-width: 0; }
.mpa-corr-c-amount { grid-area: f1; min-width: 0; }
.mpa-corr-c-total  { grid-area: f3; min-width: 0; text-align: right; }
.mpa-corr-c-action { grid-area: action; justify-self: end; align-self: center; white-space: nowrap; }
.mpa-corr-c-action .form-check { min-height: 0; }
/* Visible field label on a phone; visually hidden (still the input's
   accessible name) once the column headings take over. */
.mpa-corr-lbl {
  display: block; margin-bottom: .2rem;
  font-size: var(--mpa-text-xs); font-weight: 600; color: var(--mpa-muted);
}
/* The computed figure sits on the same line as the inputs beside it. */
.mpa-corr-amount {
  display: inline-block; font-weight: 700; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.mpa-corr-row .input-group { flex-wrap: nowrap; }
.mpa-corr-c-qty .form-control { text-align: center; }
/* Only the first "add a missing charge" row shows until another is asked
   for (or already holds a value) — three empty rows were most of the
   form's height. */
.mpa-corr-row[hidden] { display: none !important; }
.mpa-corr-add-more { margin-top: .4rem; }
.mpa-corr-row .input-group .form-control { min-width: 0; }
/* ONE control height for every box in the editor (Charge, Qty, Amount,
   descriptions, the £ prefix): Bootstrap's input-group-sm and
   form-control-sm otherwise land a pixel apart, and both are cramped. */
.mpa-corr-row .form-control,
.mpa-corr-row .input-group-text { height: 2.25rem; padding-top: .3rem; padding-bottom: .3rem; line-height: 1.4; }
.mpa-corr-amount { line-height: 2.25rem; }
.mpa-corr-row.is-amount { grid-template-areas: "desc desc desc" "f1 action action"; }
.mpa-corr-row.is-fee    { grid-template-areas: "desc desc desc" "action action f3"; }
.mpa-corr-row.is-new    { grid-template-areas: "desc desc f1"; }
.mpa-corr-row.is-amount .mpa-corr-c-action { justify-self: start; align-self: end; padding-bottom: .45rem; }
.mpa-corr-row.is-fee .mpa-corr-c-action,
.mpa-corr-row.is-rate .mpa-corr-c-action { justify-self: start; }
.mpa-corr-head { display: none; }
.mpa-corr-row.is-new:first-child { padding-top: 0; }
/* A ticked Remove / Waive: the row reads as going, not gone. */
.mpa-corr-row.is-removed .mpa-corr-name,
.mpa-corr-row.is-removed .mpa-corr-desc-input { text-decoration: line-through; opacity: .6; }
.mpa-corr-row.is-removed .mpa-corr-c-charge,
.mpa-corr-row.is-removed .mpa-corr-c-qty,
.mpa-corr-row.is-removed .mpa-corr-c-amount { opacity: .45; }
.mpa-corr-row.is-removed .mpa-corr-amount { color: var(--mpa-muted); font-weight: 600; }
@media (min-width: 768px) {
  .mpa-corr-row {
    grid-template-columns: minmax(0, 1fr) 7.5rem 5rem 7.5rem 8.5rem;
    grid-template-areas: "desc charge qty total action";
    align-items: center;
    column-gap: 1rem;
    padding: .4rem 0;
  }
  .mpa-corr-row.is-rate,
  .mpa-corr-row.is-fee { grid-template-areas: "desc charge qty total action"; }
  .mpa-corr-row.is-amount { grid-template-areas: "desc desc desc total action"; }
  .mpa-corr-row.is-amount .mpa-corr-c-action { align-self: center; padding-bottom: 0; }
  .mpa-corr-c-charge { grid-area: charge; }
  .mpa-corr-c-qty    { grid-area: qty; }
  .mpa-corr-c-amount { grid-area: total; }
  .mpa-corr-c-total  { grid-area: total; }
  .mpa-corr-c-action { justify-self: start; }
  .mpa-corr-c-amount .form-control { text-align: right; }
  .mpa-corr-lbl {
    position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .mpa-corr-head {
    display: grid; padding: 0 0 .45rem;
    font-size: var(--mpa-text-xs); font-weight: 700; color: var(--mpa-muted);
    text-transform: uppercase; letter-spacing: var(--mpa-tracking-label);
  }
  .mpa-corr-head + .mpa-corr-row { padding-top: .45rem; }
  /* Qty reads as a centred column, heading through value (the draft
     invoice table's Qty convention); money keeps its own alignment. */
  .mpa-corr-head .mpa-corr-c-qty { text-align: center; }
}

/* Summary card: the live corrected total, the reason, the actions. Sticky
   beside the lines on a desktop; last on a phone, so the actions sit at the
   bottom of the page. */
@media (min-width: 992px) {
  .mpa-corr-summary { position: sticky; top: 1rem; }
}
.mpa-corr-sum { font-variant-numeric: tabular-nums; }
.mpa-corr-sum-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: .3rem 0; color: var(--mpa-muted);
}
.mpa-corr-sum-row > span:last-child { color: var(--mpa-ink); font-weight: 600; white-space: nowrap; }
.mpa-corr-sum-row.is-total { color: var(--mpa-ink); font-weight: 700; border-top: 1px solid var(--mpa-line); border-bottom: 1px solid var(--mpa-line); padding: .5rem 0; margin: .2rem 0; }
.mpa-corr-sum-row.is-total > span:last-child { font-size: var(--mpa-text-xl); font-weight: 800; }
.mpa-corr-status { margin-top: .4rem; }
.mpa-corr-status:empty { display: none; }
.mpa-corr-status.is-muted  { color: var(--mpa-muted); }
.mpa-corr-status.is-warn   { color: var(--mpa-warn); font-weight: 600; }
.mpa-corr-status.is-danger { color: var(--mpa-danger); font-weight: 600; }
.mpa-corr-reason { margin-top: .85rem; }
.mpa-corr-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .85rem; }
.mpa-corr-actions > .btn { flex: 1 1 auto; }
@media (min-width: 992px) {
  .mpa-corr-actions { flex-direction: column; }
  .mpa-corr-actions > .btn { width: 100%; }
}

/* Review page */
.mpa-corr-changes { list-style: none; margin: 0; padding: 0; }
.mpa-corr-changes > li {
  display: flex; gap: .6rem; align-items: baseline;
  padding: .6rem 1.25rem; border-top: 1px solid var(--mpa-line); overflow-wrap: anywhere;
}
.mpa-corr-changes > li:first-child { border-top: 0; }
.mpa-corr-lines-ro { font-variant-numeric: tabular-nums; }
.mpa-corr-ro-row {
  display: flex; justify-content: space-between; gap: 1rem; align-items: baseline;
  padding: .45rem 0; border-bottom: 1px solid var(--mpa-line);
}
.mpa-corr-ro-desc { min-width: 0; overflow-wrap: anywhere; }
.mpa-corr-ro-amt { white-space: nowrap; font-weight: 600; }
.mpa-corr-ro-row.is-total { border-bottom: 0; font-weight: 700; padding-top: .6rem; }
.mpa-corr-ro-row.is-total .mpa-corr-ro-amt { font-weight: 800; }
.mpa-corr-facts { margin: .85rem 0 0; }
.mpa-corr-facts dt { font-weight: 600; color: var(--mpa-ink); margin-top: .5rem; }
.mpa-corr-facts dt:first-child { margin-top: 0; }
.mpa-corr-facts dd { margin: .1rem 0 0; color: var(--mpa-muted); overflow-wrap: anywhere; }
.mpa-corr-history .list-group-item { overflow-wrap: anywhere; }

/* ── Equal-share action row (Ticket sales header, 7 Oct 2026) ────────────
   Every VISIBLE direct child gets the same flex basis, so the row divides
   evenly whatever is shown: two actions are 50/50, three are equal thirds,
   and flex-wrap still lets a very narrow screen drop one to a new line.
   Deliberately not `flex-fill` (flex: 1 1 auto), which sizes to the label. */
.mpa-equal-actions > .btn { flex: 1 1 0; }

/* ── Enquiries & Trials V1 — manual-acceptance corrective (8 Oct 2026) ──
   Scoped to the Enquiries page, its drawer, Settings → Enquiries & trials
   and the class form. Nothing here touches another page.

   1. Add enquiry drawer. 540px on desktop so the three Request choices sit
      on one line and first/last name share a row; a phone gets the full
      width. Controls are sized to their content on a 12-column grid. */
#interest-drawer { width: 540px; max-width: 100vw; }
@media (max-width: 575.98px) { #interest-drawer { width: 100vw; } }
.mpa-enq-form .mpa-section { margin: .9rem 0 .35rem; }
.mpa-enq-form .mpa-section:first-of-type { margin-top: .6rem; }
.mpa-enq-form .mpa-field-label { white-space: normal; overflow: visible; }
.mpa-enq-opt { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--mpa-muted); opacity: .85; }
.mpa-enq-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: .5rem .75rem; align-items: end; }
.mpa-enq-grid > * { grid-column: span 12; min-width: 0; }
.mpa-enq-grid > .mpa-enq-half { grid-column: span 6; }
.mpa-enq-grid > .mpa-enq-dob { grid-column: span 7; }
.mpa-enq-grid > .mpa-enq-age { grid-column: span 3; }
.mpa-enq-grid > .mpa-enq-agehelp { margin-top: -.25rem; }
.mpa-enq-grid > .mpa-stat-note { align-self: start; }
.mpa-enq-request { margin: 0; }
.mpa-enq-request legend { float: none; width: auto; margin-bottom: .2rem; }
.mpa-enq-request .form-check { margin-bottom: .15rem; }
@media (min-width: 576px) {
  .mpa-enq-grid > .mpa-enq-email { grid-column: span 7; }
  .mpa-enq-grid > .mpa-enq-phone { grid-column: span 5; }
  .mpa-enq-grid > .mpa-enq-dob { grid-column: span 5; }
  .mpa-enq-grid > .mpa-enq-age { grid-column: span 2; }
  .mpa-enq-grid > .mpa-enq-agehelp { grid-column: span 5; align-self: center; margin-top: 1.1rem; }
  .mpa-enq-grid > .mpa-enq-source { grid-column: span 7; }
  .mpa-enq-request .form-check { display: inline-block; margin-right: 1.25rem; margin-bottom: 0; }
}

/* 2. Enquiries page — intentional, tighter vertical rhythm (measured with
      Playwright: title → note → banner → filters → list → footer) and the
      two-column action grid. Scoped to .mpa-enq-page only. */
.mpa-enq-page .mpa-page-head { margin-bottom: .7rem; }
/* The tight title -> note row gap is a desktop rhythm only. On phones the
   header keeps the shared .mpa-page-head gap, so the full-width "Add
   enquiry" sits as far below the title row as "Add class" does on Classes
   (final polish, Rory, 11 Oct 2026 — it was 3px, crowding the header). */
@media (min-width: 768px) { .mpa-enq-page .mpa-page-head { row-gap: .2rem; } }
.mpa-enq-page > .alert { margin-bottom: .6rem !important; padding-top: .55rem; padding-bottom: .55rem; }
/* Filters (corrective 3): the SHARED Students bar (.mpa-filter-bar.
   mpa-filter-flex — labelled fields, the outline "Clear" at the end, and
   the house phone stacking below 768px). Only two things are Enquiries'
   own: the page's tighter rhythm below the bar, and — with just two
   selects — a sensible desktop width instead of each stretching across
   half the page. */
.mpa-enq-page #enquiry-filters { margin-bottom: .7rem; }
/* Final polish (Rory, 11 Oct 2026): six controls — Search | Gender | Age |
   Sort | Status | From | Clear — on ONE row at desktop widths (1366 up). Each
   control has a compact width for its longest option (the search a little
   wider) and the fields don't stretch. Tablets (768-991) keep corrective
   3's wrapping fields, a few per row; phones the house full-width stack. */
@media (min-width: 768px) and (max-width: 991.98px) {
  .mpa-enq-page #enquiry-filters .mpa-filter-field { flex: 0 1 17rem; }
}
@media (min-width: 992px) {
  .mpa-enq-page #enquiry-filters .mpa-filter-field { flex: 0 1 auto; min-width: 0; }
  .mpa-enq-page #enquiry-filters #enq-q { width: 12rem !important; }
  .mpa-enq-page #enquiry-filters #enq-gender { width: 9.5rem !important; }
  .mpa-enq-page #enquiry-filters #enq-age { width: 8rem !important; }
  .mpa-enq-page #enquiry-filters #enq-sort { width: 9rem !important; }
  .mpa-enq-page #enquiry-filters #enq-status { width: 10.5rem !important; }
  .mpa-enq-page #enquiry-filters #enq-channel { width: 10rem !important; }
}
/* Mobile enquiry card: one compact block (second corrective). */
.mpa-enq-mcard > .mpa-card-body { padding: .6rem .75rem .7rem; }
.mpa-enq-m-top { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; }
.mpa-enq-m-child { min-width: 0; font-weight: 600; line-height: 1.3; }
.mpa-enq-m-child .mpa-chip { margin-left: .15rem; }
.mpa-enq-m-line { font-size: .85rem; line-height: 1.35; margin-top: .15rem; overflow-wrap: anywhere; }
.mpa-enq-m-email { font-size: .82rem; line-height: 1.35; }
.mpa-enq-m-meta { font-size: var(--mpa-text-sm); color: var(--mpa-muted); line-height: 1.35; margin-top: .1rem; }
.mpa-enq-m-note { font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-style: italic; line-height: 1.35; margin-top: .15rem; }
.mpa-enq-mcard .mpa-enq-actions { margin-top: .55rem; }
.mpa-enq-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .3rem; min-width: 15.5rem; }
.mpa-enq-actions > .btn, .mpa-enq-actions > form .btn { width: 100%; white-space: nowrap; }
.mpa-enq-actions > .mpa-enq-span, .mpa-enq-actions > .collapse, .mpa-enq-actions > .collapsing { grid-column: 1 / -1; }
.mpa-enq-actions .collapse > form, .mpa-enq-actions .collapsing > form { margin-top: 0 !important; }
@media (max-width: 767.98px) {
  .mpa-enq-actions { min-width: 0; gap: .45rem; }
  .mpa-enq-actions > .btn, .mpa-enq-actions > form .btn { display: flex; align-items: center; justify-content: center; }
}
/* "Mark as contacted" / "Choose class" / "Reopen" / "Delete…": real,
   available secondary actions — ink text and a border with >= 3:1 contrast
   against the card (the house neutral outline's #eae7f1 border read as a
   disabled control). Hover and keyboard focus are both visible. */
.mpa-enq-actions .mpa-enq-secondary {
  --bs-btn-color: var(--mpa-ink);
  --bs-btn-border-color: #8f8a9e;
  --bs-btn-bg: var(--mpa-surface);
  --bs-btn-hover-color: var(--mpa-ink);
  --bs-btn-hover-bg: var(--mpa-track);
  --bs-btn-hover-border-color: var(--mpa-ink);
  --bs-btn-active-bg: var(--mpa-line);
  --bs-btn-active-border-color: var(--mpa-ink);
}
.mpa-enq-actions .mpa-enq-delete { --bs-btn-color: var(--mpa-danger-ink); --bs-btn-hover-color: var(--mpa-danger-ink); }
.mpa-enq-actions .btn:focus-visible { outline: 2px solid var(--mpa-ink); outline-offset: 2px; box-shadow: none; }

/* 3. Settings → Enquiries & trials. Two stacked cards (round 2); inside,
      a compact number field beside its scope choice, and the switches
      beside the published list.
      The URL row and its one-line help may use the card's full width. */
.mpa-enq-set-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--set-gap, .75rem); align-items: start; margin-bottom: var(--set-gap, .75rem); }
/* Round 2 (10 Oct 2026): the two cards stack at every width — side by side the short Trial classes
   card had to be stretched to the Public card's height, leaving its controls floating in an empty
   card. The shared Save footer (.set-actionbar, the grid's last row) sits directly under both. */
.mpa-enq-limit { flex: 0 0 auto; }
.mpa-enq-limit .form-control { width: 7.5rem; }
.mpa-enq-scope { flex: 1 1 16rem; min-width: 0; }
.mpa-enq-scope .form-check { margin-bottom: .1rem; }
/* Same specificity as the Settings 75ch measure rule (+1), so these two
   blocks may match the URL row's width instead. */
main.main-content:has(.settings-group-nav) .mpa-card-body .mpa-enq-url div.form-text,
main.main-content:has(.settings-group-nav) .mpa-card-body p.small.mpa-enq-intro,
main.main-content:has(.settings-group-nav) .mpa-card-body p.small.mpa-enq-public-help { max-width: 52rem; }
.mpa-enq-public-help { grid-column: 1 / -1; }
.mpa-enq-url .input-group { max-width: 52rem; }
.mpa-enq-public-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem 2rem; margin-top: .6rem; }
.mpa-enq-public-classes .mpa-section { margin-top: 0; }
@media (max-width: 767.98px) { .mpa-enq-public-classes { border-top: 1px solid var(--mpa-hairline); padding-top: .75rem; } }
@media (max-width: 575.98px) {
  /* Phones: the long URL gets the full width and Copy sits beneath it. */
  .mpa-enq-url .input-group { flex-wrap: wrap; gap: .4rem; }
  .mpa-enq-url .input-group > .form-control,
  .mpa-enq-url .input-group > .btn { flex: 1 1 100%; width: 100%; margin-left: 0 !important; border-radius: var(--mpa-radius-sm) !important; }
}

/* 4. Class form (Edit / Add class). Content-sized columns on Bootstrap's
      own wrapping row: numbers are bounded to what they hold, names, venues
      and teachers keep room, textareas stay full width. */
@media (min-width: 768px) {
  .mpa-cls-fields > .mpa-fw-type { width: 13rem; }
  .mpa-cls-fields > .mpa-fw-num { width: 9rem; }
  .mpa-cls-fields > .mpa-fw-flex { min-width: 18rem; }
}
/* Where / what (final acceptance, 10 Oct 2026): Venue · cost override · staffing estimate, with
   Discipline directly under Venue + cost — the staffing note spans both rows, so it never pushes
   Discipline a whole row down. Gaps match the row's own g-3 gutter. */
.mpa-cls-place { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.mpa-cls-place .form-label { margin-bottom: var(--mpa-space-tight); }
.mpa-cls-place .form-text { margin-top: var(--mpa-space-tight); }
.mpa-cls-place > .mpa-fw-money > .form-control { max-width: 11rem; }
.mpa-cls-place > .mpa-fw-discipline > .form-select { max-width: 22rem; }
@media (min-width: 768px) {
  .mpa-cls-place { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .mpa-cls-place > .mpa-fw-money > .form-control { max-width: 13rem; }
}
@media (min-width: 1200px) {
  .mpa-cls-place { grid-template-columns: minmax(15rem, 27rem) 13rem minmax(15rem, 23rem); align-items: start; }
  .mpa-cls-place > .mpa-fw-venue { grid-column: 1; grid-row: 1; }
  .mpa-cls-place > .mpa-fw-money { grid-column: 2; grid-row: 1; }
  .mpa-cls-place > .mpa-fw-teacher { grid-column: 3; grid-row: 1 / span 2; }
  .mpa-cls-place > .mpa-fw-discipline { grid-column: 1 / span 2; grid-row: 2; }
}
@media (min-width: 992px) { .mpa-cls-fields > .mpa-fw-num { width: 10rem; } }
@media (max-width: 991.98px) {
  /* the three numbers share a row: equal (two-line) label heights keep the
     inputs aligned, and the required * stays inline after its words */
  .mpa-cls-fields > :is(.mpa-fw-num, .mpa-fw-type) .form-label { min-height: 2.9em; display: flex; align-items: flex-end; column-gap: .25em; }
}
/* Class image: room for a real filename, the action beside it; both full
   width on a phone. */
.mpa-cls-upload-row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.mpa-cls-upload { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; flex: 1 1 26rem; max-width: 40rem; min-width: 0; }
.mpa-cls-upload > input[type=file] { flex: 1 1 18rem; min-width: 0; }
.mpa-cls-upload > .btn { flex: 0 0 auto; }
@media (max-width: 575.98px) {
  .mpa-cls-upload-row, .mpa-cls-upload { flex-direction: column; align-items: stretch; }
  .mpa-cls-upload { flex: 1 1 auto; max-width: none; }
  .mpa-cls-upload > input[type=file], .mpa-cls-upload > .btn, .mpa-cls-upload-row > form > .btn { flex: 0 0 auto; width: 100%; }
}
/* Phones: the lone Deactivate action takes the full action row (the house
   header grid's "odd last action spans" rule targets the <form>, which is
   display:contents, so it never reached the button). */
@media (max-width: 767.98px) {
  .mpa-detail-head .mpa-detail-actions > form.mpa-cls-deactivate:last-child:nth-child(odd) > .btn { grid-column: 1 / -1; }
}
/* Phones: denser class form — less padding and fewer empty gaps, same
   touch-target controls. */
@media (max-width: 767.98px) {
  .mpa-cls-form > .card-body { padding: .9rem .85rem 1rem; }
  .mpa-cls-form .row.g-3 { --bs-gutter-y: .7rem; --bs-gutter-x: .6rem; }
  .mpa-cls-form .row.g-2 { --bs-gutter-y: .45rem; }
  .mpa-cls-form .row.mb-4 { margin-bottom: 1rem !important; }
  .mpa-cls-form .row.mb-3 { margin-bottom: .8rem !important; }
  .mpa-cls-form h6.mb-3 { margin-bottom: .6rem !important; padding-bottom: .35rem !important; }
  .mpa-cls-form .alert.mb-3 { margin-bottom: .7rem !important; }
  .mpa-cls-form .form-label { margin-bottom: .2rem; }
  .mpa-cls-form .form-text { margin-top: .2rem; }
  .mpa-cls-form p.mb-3 { margin-bottom: .7rem !important; }
}

/* 5. Second manual-acceptance corrective (8 Oct 2026).
      Arrange a trial / Enrol pupil drawer: task-focused. The status pill
      shares the identity line; the facts under the date are two short
      lines; nothing explains what happens afterwards. */
.mpa-enq-act-head { margin-bottom: .6rem; }
.mpa-enq-ident { display: flex; align-items: center; flex-wrap: wrap; gap: .25rem .5rem; color: var(--mpa-muted); font-size: var(--mpa-text-sm); }
.mpa-enq-ident-text { min-width: 0; overflow-wrap: anywhere; }
.mpa-enq-act-when { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0 .75rem; }
@media (min-width: 576px) { .mpa-enq-act-when { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); } }
.mpa-enq-act-dob .form-control { max-width: 12rem; }
.mpa-enq-act-facts { font-size: var(--mpa-text-sm); color: var(--mpa-muted); line-height: 1.4; margin: .1rem 0 .8rem; }
.mpa-enq-act-facts > div + div { margin-top: .1rem; }
@media (max-width: 575.98px) {
  .mpa-enq-act-buttons > .btn { flex: 1 1 0; min-height: 44px; }
}
/* Settings → Enquiries & trials: the ONE Save for both cards is the grid's .set-actionbar footer
   (defined with the Settings primitives). Phones: a full-width 44px Save. */
@media (max-width: 575.98px) {
  .mpa-enq-save-row > .btn { width: 100%; min-height: 44px; }
}

/* 6. Third manual-acceptance corrective (9 Oct 2026). Scoped to the
      Enquiries page and its drawer; every rule here names an mpa-enq-* /
      mpa-uk-date class, so no other page can match.

   Add enquiry — section headings in the school's brand ink (the same
   uppercase micro-label, coloured, not a new type style). "Who is this
   for?" is a <fieldset>/<legend>; Bootstrap floats and enlarges legends,
   which is undone here so it reads exactly like the other headings. */
.mpa-enq-form .mpa-enq-heading { color: var(--mpa-ink-brand); }
.mpa-enq-who { margin: 0; padding: 0; border: 0; min-width: 0; }
.mpa-enq-who > legend.mpa-section { float: none; width: 100%; padding: 0; font-size: inherit; line-height: inherit; }
.mpa-enq-form .mpa-enq-who > legend.mpa-section { margin-top: .2rem; }
.mpa-enq-who-toggle > .btn { display: inline-flex; align-items: center; }
@media (max-width: 575.98px) {
  .mpa-enq-who-toggle { display: flex; width: 100%; }
  .mpa-enq-who-toggle > .btn { flex: 1 1 0; justify-content: center; min-height: 44px; }
}

/* Existing pupil: the search results and the picked pupil. Results are a
   bordered list of full-width buttons (keyboard reachable; each a 44px
   target on touch); the picked pupil is one quiet summary box. */
.mpa-enq-results:not(:empty) { margin-top: .35rem; }
.mpa-enq-result-list { border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius-sm); overflow: hidden; background: var(--mpa-surface); }
.mpa-enq-result {
  display: flex; flex-direction: column; align-items: flex-start; gap: .05rem; width: 100%;
  padding: .45rem .7rem; border: 0; background: transparent; text-align: left; color: var(--mpa-ink);
}
.mpa-enq-result + .mpa-enq-result { border-top: 1px solid var(--mpa-hairline); }
.mpa-enq-result:hover { background: var(--mpa-tint); }
.mpa-enq-result:focus-visible { outline: 2px solid var(--mpa-ink); outline-offset: -2px; }
.mpa-enq-result-name { font-weight: 600; font-size: var(--mpa-text-md); }
.mpa-enq-result-meta { font-size: var(--mpa-text-sm); color: var(--mpa-muted); overflow-wrap: anywhere; }
@media (hover: none) and (pointer: coarse) { .mpa-enq-result { min-height: 44px; justify-content: center; } }
.mpa-enq-picked {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem;
  padding: .55rem .75rem; border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius-sm); background: var(--mpa-surface);
}
.mpa-enq-picked-main { min-width: 0; }
.mpa-enq-picked-name { font-weight: 700; }
.mpa-enq-picked-meta { font-size: var(--mpa-text-sm); color: var(--mpa-muted); line-height: 1.35; overflow-wrap: anywhere; }
.mpa-enq-picked > .btn { flex: 0 0 auto; }

/* New pupil: the parent email already belongs to a family. An info
   panel (the house info tint), the family's pupils as buttons and the
   "new pupil in this family" choice beside them; wraps on a phone. */
.mpa-enq-family, .mpa-enq-family-ack {
  background: var(--mpa-info-bg); color: var(--mpa-info-ink); border-radius: var(--mpa-radius-sm);
  padding: .5rem .7rem; font-size: var(--mpa-text-sm); line-height: 1.4;
}
.mpa-enq-family-q { overflow-wrap: anywhere; }
.mpa-enq-family-portal { opacity: .85; }
.mpa-enq-family-actions { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .4rem; }
.mpa-enq-family-actions > .btn { background: var(--mpa-surface); }
/* "Add a new pupil to this family" is a real choice, not a disabled one:
   the same ink text + >= 3:1 border as the list's secondary actions. */
.mpa-enq-family-actions > .btn-outline-secondary {
  --bs-btn-color: var(--mpa-ink); --bs-btn-border-color: #8f8a9e;
  --bs-btn-hover-color: var(--mpa-ink); --bs-btn-hover-bg: var(--mpa-track); --bs-btn-hover-border-color: var(--mpa-ink);
}
@media (max-width: 575.98px) { .mpa-enq-family-actions > .btn { min-height: 44px; flex: 1 1 auto; } }

/* Typed UK date (classes/_uk_date.html): the text box is the field; the
   calendar button sits inside its right edge; the native picker it opens
   is present (showPicker needs a rendered element) but invisible and
   anchored to the field so the calendar opens beside it. */
.mpa-uk-date { position: relative; }
.mpa-uk-date > .form-control { padding-right: 2.4rem; font-variant-numeric: tabular-nums; }
.mpa-uk-date-pick {
  position: absolute; top: 0; right: 0; bottom: 0; width: 2.3rem;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--mpa-muted); border-radius: 0 var(--mpa-radius-sm) var(--mpa-radius-sm) 0;
}
.mpa-uk-date-pick:hover { color: var(--mpa-ink); }
.mpa-uk-date-pick:focus-visible { outline: 2px solid var(--mpa-ink); outline-offset: -2px; }
.mpa-uk-date-native {
  position: absolute; right: 0; bottom: 0; width: 1px; height: 1px;
  opacity: 0; pointer-events: none; border: 0; padding: 0;
}
/* The DOB/Age pair is content-sized on desktop: the date needs ~10
   characters plus its button, the age two digits. The one-line help sits
   directly UNDER the pair at every width — beside it, wherever it wrapped
   (768px on touch), it set the row's height and, the row being bottom-
   aligned, pushed the Date of birth label ~17px down (measured by
   qa/test_layout_spacing_regression.py). */
@media (min-width: 576px) {
  .mpa-enq-grid > .mpa-enq-dob { grid-column: span 4; }
  .mpa-enq-grid > .mpa-enq-age { grid-column: span 2; }
  .mpa-enq-grid > .mpa-enq-agehelp { grid-column: span 12; align-self: start; margin-top: -.25rem; }
}
.mpa-enq-act-dob .mpa-uk-date { max-width: 12rem; }
.mpa-enq-act-dob .mpa-uk-date > .form-control { max-width: none; }

/* In-flight submit (Add enquiry, Arrange trial, Enrol pupil). The house
   .mpa-gen-idle/.mpa-gen-working label swap runs on htmx's own
   .htmx-request class (no bespoke state); here the fields dim and stop
   taking input while the submit button and the "this can take a few
   seconds" line stay fully legible. .mpa-enq-done holds that state from
   a successful response until the page has navigated to the result. */
.mpa-enq-busy.htmx-request, .mpa-enq-busy.mpa-enq-done { pointer-events: none; }
.mpa-enq-busy.htmx-request > :not(.mpa-enq-act-buttons):not(.mpa-enq-busy-note),
.mpa-enq-busy.mpa-enq-done > :not(.mpa-enq-act-buttons):not(.mpa-enq-busy-note) { opacity: .55; transition: opacity .15s; }
.mpa-enq-busy.htmx-request button[type=submit]:disabled,
.mpa-enq-busy.mpa-enq-done button[type=submit] { opacity: 1; }
.mpa-enq-done .mpa-gen-idle { display: none; }
.mpa-enq-done .mpa-gen-working { display: inline-flex; align-items: center; gap: .35rem; }
.mpa-enq-busy-note { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin-top: .4rem; }
.mpa-enq-busy:not(.htmx-request):not(.mpa-enq-done) .mpa-enq-busy-note { display: none; }

/* Arrange trial / Enrol pupil — the ONE "is this one of their pupils?"
   question for an enquiry not yet linked to a pupil. */
.mpa-enq-match { padding: .5rem .7rem; border: 1px solid var(--mpa-line); border-radius: var(--mpa-radius-sm); }
.mpa-enq-match-q { font-weight: 600; font-size: var(--mpa-text-md); margin-bottom: .3rem; overflow-wrap: anywhere; }
.mpa-enq-match .form-check { margin-bottom: .15rem; }
.mpa-enq-match-meta { display: block; font-size: var(--mpa-text-sm); color: var(--mpa-muted); }

/* Enquiries list. The invitation notice: Copy invite link on the
   explanation's own row where it fits, beneath it on a phone. */
.mpa-enq-notice { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem 1rem; }
.mpa-enq-notice-text { flex: 1 1 24rem; min-width: 0; }
.mpa-enq-notice-action { flex: 0 0 auto; }
/* A child's name has ONE typography whether or not they are a pupil yet;
   only a real pupil is a link. */
.mpa-enq-child { font-weight: 600; color: var(--mpa-ink); }
a.mpa-enq-child-link { color: var(--mpa-ink-brand); text-decoration: none; }
a.mpa-enq-child-link:hover, a.mpa-enq-child-link:focus-visible { text-decoration: underline; }
.mpa-enq-child-age { color: var(--mpa-muted); }
.mpa-enq-unreg { font-size: var(--mpa-text-2xs); margin-left: .25rem; vertical-align: .1em; white-space: nowrap; }
/* The family's note under the name: one quiet line, without the
   .mpa-stat-note .9rem top margin that opened the gap Rory saw. */
.mpa-enq-note {
  font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-style: italic;
  line-height: 1.35; margin-top: .15rem; max-width: 26ch; white-space: normal;
}

/* ══ 7. UI DENSITY / SPACING STANDARD (9 Oct 2026) ══════════════════════════════════════════════
   The standing rule is docs/UI_DENSITY_STANDARD.md (PROJECT_CONTEXT §9.3.1 rule 16); the tiers are
   the EXISTING tokens — no new scale:
     MICRO    --mpa-space-tight  .35rem   label → control, a heading → its first row, inline meta
     RELATED  --mpa-space-group  .5rem    control → help, row → row of one group, header rows
     SECTION  --mpa-space-block  1rem (phone .75rem)   field group → group, block → block, card → card
     MAJOR    --mpa-space-section 1.35rem (phone 1rem) major page region → the next
   Touch target height is NOT visual gap: a 44px hit area must not ADD padding/margins on top of
   itself (see .mpa-drawer-section). Guarded by qa/test_layout_spacing_regression.py.            */

/* Timetable — the term context. Phones: ONE white card holding the term select, the filter-sheet
   trigger beside it, and the term's dates under it. */
.mpa-tt-term-row { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.mpa-tt-term-row > form { min-width: 0; }
.mpa-tt-filter-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; gap: .35rem;
  min-width: 44px; min-height: 42px; padding: 0 .7rem; font-size: var(--mpa-text-sm);
}
.mpa-tt-filter-btn .badge { font-size: .7rem; }
.mpa-tt-term-range { margin: 0; }
@media (max-width: 767.98px) {
  /* select on its own full-width line (a long term name is never truncated); dates on the second
     line with the Filters trigger at its end */
  .mpa-page-head.mpa-tt-head > .mpa-term-block:has(select) {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: .3rem .5rem;
    padding: .5rem .65rem;
  }
  .mpa-tt-term-row { display: contents; }
  .mpa-tt-term-row > form { grid-column: 1 / -1; grid-row: 1; }
  .mpa-tt-filter-btn { grid-column: 2; grid-row: 2; min-height: 36px; }
  .mpa-tt-term-range { grid-column: 1; grid-row: 2; }
}

/* Timetable — week navigator (phones): the label takes the width between the arrows, so
   "5 – 11 Oct · This week" is never truncated at phone widths; arrows pinned to the ends, so
   they still never move between weeks. */
@media (max-width: 767.98px) {
  .mpa-tt-weeknav .mpa-weeknav-controls > .position-relative { flex: 1 1 auto; min-width: 0; }
  .mpa-tt-weeknav .mpa-weeknav-label { width: 100%; font-size: var(--mpa-text-sm); }
  .mpa-tt-weeknav .mpa-weeknav-controls > .btn { min-width: 44px; min-height: 36px; }
}

/* Timetable — footer: venue pills wrap on a compact, deliberate rhythm; Customise colours and Key
   share ONE action row under them; the Key's content opens below that row. */
.mpa-tt-venues { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .4rem; margin-top: var(--mpa-space-block); }
.mpa-tt-venues-label { font-size: var(--mpa-text-sm); font-weight: 600; color: var(--mpa-muted); margin-right: .15rem; }
.mpa-tt-foot-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin-top: var(--mpa-space-group); }
@media (max-width: 767.98px) {
  .mpa-tt-foot-actions > .btn { min-height: 36px; }
}

/* ══ 8. ENQUIRIES & TRIALS — MANUAL ACCEPTANCE ROUND 2 (10 Oct 2026) ═════════════════════════════
   Built to the density standard (section 7 / docs/UI_DENSITY_STANDARD.md): existing tokens only,
   no negative margins, no viewport-specific offsets.                                            */

/* Class form — ONE "Public-facing class information" section (listing, texts, ages + places,
   price shown, image). Rhythm: heading → MICRO → intro → SECTION → each group. */
.mpa-cls-public { margin-bottom: var(--mpa-space-section); }
.mpa-cls-public-title { margin-bottom: var(--mpa-space-tight); }
.mpa-cls-public-intro { font-size: var(--mpa-text-sm); color: var(--mpa-muted); max-width: 52rem;
                        margin: 0 0 var(--mpa-space-block); }
.mpa-cls-public-listing { margin-bottom: var(--mpa-space-block); }
.mpa-cls-public-listing .form-text { margin-top: .15rem; }
.mpa-cls-public-texts { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--mpa-space-block) 1rem;
                        margin-bottom: var(--mpa-space-block); }
@media (min-width: 768px) {
  .mpa-cls-public-texts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mpa-cls-public-desc { grid-column: 1 / -1; }
}
/* Ages + Places: label row then control row, so the radios sit level with the age boxes. */
.mpa-cls-public-who { display: grid; grid-template-columns: repeat(2, minmax(0, 8.5rem)); align-items: end;
                      gap: var(--mpa-space-group) 1rem; margin-bottom: var(--mpa-space-block); }
.mpa-cls-public-who .form-label { margin-bottom: var(--mpa-space-tight); }
.mpa-cls-places { grid-column: 1 / -1; margin: 0; min-width: 0; }
.mpa-cls-places > legend.form-label { float: none; width: auto; padding: 0; line-height: inherit; }
.mpa-cls-places-opts { display: flex; flex-wrap: wrap; align-items: center; gap: .15rem 1.25rem;
                       min-height: calc(1.5em + .75rem + 2px); }
.mpa-cls-places-opts .form-check { margin: 0; }
.mpa-cls-age-help, .mpa-cls-places-help { grid-column: 1 / -1; margin-top: 0; max-width: 52rem; }
/* phones: each note directly under its own control (ages → their note → Places → its note) */
@media (max-width: 767.98px) {
  .mpa-cls-age-help { order: 1; }
  .mpa-cls-places { order: 2; }
  .mpa-cls-places-help { order: 3; }
}
@media (min-width: 768px) {
  .mpa-cls-public-who { grid-template-columns: 8.5rem 8.5rem minmax(0, 1fr); }
  .mpa-cls-places { grid-column: auto; }
}
.mpa-cls-image { border-top: 1px solid var(--mpa-hairline); padding-top: var(--mpa-space-block); }
.mpa-cls-image > .form-label { display: block; margin-bottom: var(--mpa-space-tight); }
.mpa-cls-image-body { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--mpa-space-group) 1rem; }
.mpa-cls-image-ctl { flex: 1 1 22rem; min-width: 0; }
@media (max-width: 575.98px) {
  .mpa-cls-upload-row > .btn { width: 100%; }
}
/* Final pass (10 Oct 2026): the visibility exceptions sit together — "Exclude this class from the
   public page" with "Exclude price …" directly under it. */
.mpa-cls-public-listing .form-check + .form-check,
.mpa-cls-public-listing [x-show] + .form-check { margin-top: var(--mpa-space-tight); }

/* Add enquiry — staff intent completes in this flow. The primary button names what will happen
   (Save enquiry / Arrange trial / Enrol pupil): the form's data-action picks the label. */
#enquiry-add-form [data-cta] { display: none; }
#enquiry-add-form[data-action="save"] [data-cta="save"],
#enquiry-add-form[data-action="trial"] [data-cta="trial"],
#enquiry-add-form[data-action="enrol"] [data-cta="enrol"] { display: inline; }
#enquiry-add-form[data-action="save"] .mpa-enq-sibling { display: none; }
@media (min-width: 576px) {
  .mpa-enq-grid > .mpa-enq-when { grid-column: span 7; }
  .mpa-enq-grid > .mpa-enq-length { grid-column: span 5; }
}
.mpa-enq-form .mpa-enq-act-facts { margin: var(--mpa-space-tight) 0 0; }
.mpa-enq-allowance { color: var(--mpa-warn-ink); background: var(--mpa-warn-bg); border: 1px solid var(--mpa-warn);
                     border-radius: var(--mpa-radius-sm); padding: .45rem .65rem; margin-bottom: var(--mpa-space-tight); }
.mpa-enq-allowance .form-check-label { color: var(--mpa-ink); font-weight: 600; }
.mpa-enq-family-name { margin-top: var(--mpa-space-tight); overflow-wrap: anywhere; }
.mpa-enq-family-ack .form-check-label { font-size: var(--mpa-text-sm); }
/* The searchable pupil picker: the house .mpa-combo dropdown (surface, border, shadow, cap) with
   two-line rows; .on = the row the arrow keys are on. */
.mpa-enq-combo { min-width: 0; }
.mpa-enq-combo-list { max-height: min(320px, 50vh); }
.mpa-enq-combo-list .mpa-enq-result { border-radius: 7px; padding: .4rem .55rem; }
.mpa-enq-combo-list .mpa-enq-result + .mpa-enq-result { border-top: 0; }
.mpa-enq-combo-list .mpa-enq-result.on { background: var(--mpa-track); }

/* Enquiries table — sortable headers (the Reports pattern): the label is the link; the arrow is
   quiet until its column is the active sort. */
.mpa-sort-link { display: inline-flex; align-items: center; gap: .1rem; color: inherit; text-decoration: none; white-space: nowrap; }
.mpa-sort-link:hover, .mpa-sort-link:focus-visible { color: var(--mpa-ink); text-decoration: underline; text-underline-offset: 2px; }
.mpa-sort-link > i { opacity: 0; transition: opacity .12s; }
.mpa-sort-link:hover > i, .mpa-sort-link:focus-visible > i, .mpa-sort-link.is-on > i { opacity: 1; }
.mpa-sort-link.is-on { color: var(--mpa-ink); }
/* Settings → Enquiries & trials (round 2): the two cards stack; Trial classes is ONE compact band —
   number · the two ways of counting as side-by-side choices · the rule. No stretched card. */
.mpa-enq-trial-form { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--mpa-space-block) 1.75rem; align-items: start; }
.mpa-enq-trial-form .mpa-field-label { margin-bottom: var(--mpa-space-tight); }
.mpa-enq-scope { margin: 0; min-width: 0; }
.mpa-enq-scope > legend { float: none; width: auto; padding: 0; }
.mpa-enq-scope-opts { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--mpa-space-group); }
.mpa-enq-scope-opt { margin: 0; padding: .45rem .7rem .45rem 2.05rem; border: 1px solid var(--mpa-line);
                     border-radius: var(--mpa-radius-sm); background: var(--mpa-surface); }
.mpa-enq-scope-opt:has(.form-check-input:checked) { border-color: var(--mpa-action); background: var(--mpa-tint); }
.mpa-enq-scope-opt .form-check-label { display: block; }
.mpa-enq-trial-help { max-width: 34rem; }
@media (min-width: 576px) { .mpa-enq-scope-opts { grid-template-columns: repeat(2, minmax(0, 17rem)); } }
/* From tablet: number | choices, with the rule directly under the choices it explains. */
@media (min-width: 768px) {
  .mpa-enq-trial-form { grid-template-columns: auto minmax(0, 1fr); row-gap: var(--mpa-space-group); }
  .mpa-enq-trial-help { grid-column: 2; }
}

/* ══ 9. ENQUIRIES & TRIALS — FINAL MANUAL ACCEPTANCE (10 Oct 2026, late) ═══════════════════════════
   Existing tokens only, no negative margins.                                                      */

/* Settings → Enquiries & trials, denser: Public card (switches, then the classes on the page right
   under them) on the left; a COMPACT Trial classes card on the right from 1200px; stacked below.
   Neither card is stretched to the other (align-items: start); the one Save spans both. */
.mpa-enq-public-grid { row-gap: var(--mpa-space-block); }
.mpa-enq-public-classes { border-top: 1px solid var(--mpa-hairline); padding-top: var(--mpa-space-group); }
@media (min-width: 1200px) {
  .mpa-enq-set-grid { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
  .mpa-enq-set-grid > .set-actionbar { grid-column: 1 / -1; }
}
.mpa-enq-trial-card .mpa-enq-trial-form { display: flex; flex-direction: column; gap: var(--mpa-space-block); }
.mpa-enq-trial-card .mpa-enq-scope { flex: 0 0 auto; }   /* its 16rem row basis would become height here */
.mpa-enq-trial-card .mpa-enq-scope-opt { padding: 0 0 0 1.6rem; border: 0; background: none; margin: 0 0 var(--mpa-space-tight); }
.mpa-enq-trial-card .mpa-enq-scope-opt:has(.form-check-input:checked) { background: none; }
.mpa-enq-trial-card .mpa-enq-scope-help { font-size: var(--mpa-text-sm); }
.mpa-enq-trial-card .mpa-enq-trial-help { max-width: none; }

/* Class image (final acceptance): a clean public-style 16:9 preview with "Edit framing" beside it;
   the help line under both uses the section's full width. The framing formula is
   services/public_school.image_style (absolute image inside a clipping 16:9 frame). */
.mpa-frame { position: relative; overflow: hidden; aspect-ratio: 16 / 9; background: var(--mpa-tint);
             border-radius: var(--mpa-radius-sm); border: 1px solid var(--mpa-hairline); }
.mpa-frame-img { position: absolute; max-width: none; height: auto; display: block; user-select: none; -webkit-user-drag: none; }
.mpa-cls-image-preview-wrap { flex: 0 0 18rem; max-width: 100%; }
.mpa-cls-image-ctl { display: flex; flex-direction: column; align-items: flex-start; gap: var(--mpa-space-group); }
.mpa-cls-image-help { margin-top: var(--mpa-space-group); max-width: none; }
main.main-content .mpa-cls-image .form-text.mpa-cls-image-help { max-width: none; }
@media (max-width: 575.98px) {
  .mpa-cls-image-preview-wrap { flex-basis: 100%; }
  .mpa-cls-image-ctl { align-items: stretch; }
  .mpa-cls-frame-btn { min-height: 44px; }
}
/* The Edit framing dialog: the faint whole photo behind, the bright 16:9 window = what families see. */
.mpa-crop-stage { position: relative; overflow: hidden; border-radius: var(--mpa-radius-sm);
                  background: repeating-conic-gradient(var(--mpa-tint) 0% 25%, var(--mpa-surface) 0% 50%) 0 0 / 16px 16px; }
.mpa-crop-ghost { position: absolute; max-width: none; height: auto; opacity: .32; user-select: none; -webkit-user-drag: none; pointer-events: none; }
.mpa-crop-window { position: absolute; overflow: hidden; background: var(--mpa-tint); cursor: grab; touch-action: none;
                   outline: 2px solid var(--mpa-action); box-shadow: 0 0 0 9999px rgba(32, 29, 41, .18); }
.mpa-crop-window.is-dragging { cursor: grabbing; }
.mpa-crop-main { position: absolute; max-width: none; height: auto; display: block; user-select: none; -webkit-user-drag: none; pointer-events: none; }
.mpa-crop-ctl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .15rem .9rem; align-items: center; margin-top: var(--mpa-space-block); }
.mpa-crop-label { font-size: var(--mpa-text-sm); font-weight: 600; margin: 0; white-space: nowrap; }
.mpa-crop-ctl .form-range { min-width: 0; }
@media (hover: none) and (pointer: coarse) {
  .mpa-crop-ctl .form-range { height: 2.75rem; }
  .mpa-crop-ctl .form-range::-webkit-slider-thumb { width: 1.6rem; height: 1.6rem; margin-top: -.55rem; }
  .mpa-crop-ctl .form-range::-moz-range-thumb { width: 1.6rem; height: 1.6rem; }
  .mpa-crop-foot .btn { min-height: 44px; }
}
@media (max-width: 575.98px) { .mpa-crop-foot { flex-wrap: wrap; } .mpa-crop-foot [data-crop-reset] { flex: 1 1 100%; margin: 0 0 var(--mpa-space-tight) !important; } }

/* ══ 10. ENQUIRIES & TRIALS — FINAL ACCEPTANCE UX PASS (11 Oct 2026) ═══════════════════════════════
   Existing tokens only.                                                                             */

/* Settings: the trial maximum is a one- or two-digit number — a box that size, "Leave blank for no
   limit" beside it. 44px tall on touch screens like the other phone controls. */
.mpa-enq-limit-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--mpa-space-tight) var(--mpa-space-group); }
.mpa-enq-limit .form-control.mpa-enq-limit-input { width: 5rem; flex: 0 0 auto; }
.mpa-enq-limit-hint { font-size: var(--mpa-text-sm); }
@media (hover: none) and (pointer: coarse) { .mpa-enq-limit .form-control.mpa-enq-limit-input { min-height: 44px; } }

/* Settings: "Classes on the public page (N)" — a disclosure (native <details>, the chevron of the
   collapsible cards), the count always visible; a full-width 44px row on touch screens. */
.mpa-enq-pubcls > summary { list-style: none; cursor: pointer; display: flex; align-items: center;
                            gap: var(--mpa-space-tight); width: fit-content; max-width: 100%; }
.mpa-enq-pubcls > summary::-webkit-details-marker { display: none; }
.mpa-enq-pubcls > summary:focus-visible { outline: 2px solid var(--mpa-info); outline-offset: 2px; border-radius: 4px; }
.mpa-enq-pubcls[open] > summary .mpa-collapse-chevron { transform: rotate(180deg); }
.mpa-enq-pubcls-body { padding-top: var(--mpa-space-tight); }
@media (hover: none) and (pointer: coarse) { .mpa-enq-pubcls > summary { min-height: 44px; width: 100%; } }

/* Add enquiry: the family-match line is a grid row only when it says something — empty, it added a
   second row gap between the email's help line and the CHILD heading. */
#ea-family-match:empty { display: none; }

/* Enquiries: the status pill with its trial start date (or other detail) underneath, as ONE centred
   stack — the date sat a full line away, so the pill read as off-centre in its cell. */
.mpa-enq-status { display: inline-flex; flex-direction: column; align-items: center; gap: .15rem; vertical-align: middle; }
.mpa-enq-status-sub { font-size: var(--mpa-text-sm); color: var(--mpa-muted); line-height: 1.2; white-space: nowrap; }
.mpa-enq-m-top .mpa-enq-status { flex-shrink: 0; }

/* Parent portal report card (Marketing Website V1.2 final acceptance, 13 Oct 2026, Rory): on phones
   the term/sent line followed the title into the column beside the hamburger, floating indented
   under the name while the back link below sat on the content edge. Same rule as the admin detail
   heads (.mpa-head-reclaim): only the title sits beside the hamburger; the metadata line takes the
   full content width, on the same edge as the back link, with a little less space before it.
   Phones only; desktop keeps the inline title + metadata row. */
@media (max-width: 767.98px) {
  .mpa-page-head.mpa-head-meta-full { row-gap: .35rem; margin-bottom: .6rem; }
  .mpa-page-head.mpa-head-meta-full > .mpa-title-row { display: contents; }
  .mpa-page-head.mpa-head-meta-full .mpa-page-title { grid-column: 2; grid-row: 1; margin: 0; }
  .mpa-page-head.mpa-head-meta-full .mpa-greeting-inline { grid-column: 1 / -1; grid-row: 2; }
}

/* AUTOMATIC COLLECTIONS V1 (21 Oct 2026): the parent portal's Direct Debit
   page and the admin invoice collection panel. The explainer list keeps a
   compact rhythm (UI density standard).
   22 Oct 2026 (Rory's walkthrough): the cards fill the standard portal
   content width (the page gutters are .portal-wrap's own) instead of a 720px
   cap that left half the desktop page empty; only the TEXT keeps a readable
   measure (.mpa-dd-text). All mandate actions share one action area at the
   foot of the status card (.mpa-dd-actions): primary first, Cancel Direct
   Debit beside it as an outline-danger secondary; phones stack them full
   width with 44px targets. */
.mpa-dd-text { max-width: 68ch; }
.mpa-dd-actions { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--mpa-hairline, #eee); }
.mpa-dd-buttons { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
@media (max-width: 575.98px) {
  .mpa-dd-buttons { flex-direction: column; align-items: stretch; }
  .mpa-dd-buttons > form, .mpa-dd-buttons > form > .btn, .mpa-dd-buttons > .btn { width: 100%; }
  .mpa-dd-buttons .btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
}
.mpa-dd-points { padding-left: 1.1rem; margin: 0; font-size: var(--mpa-text-md); color: var(--mpa-muted); }
.mpa-dd-points li + li { margin-top: .25rem; }
.dd-panel .dd-attempts { font-size: var(--mpa-text-sm, .875rem); }
.dd-panel .dd-attempt + .dd-attempt { margin-top: .3rem; padding-top: .3rem; border-top: 1px solid var(--mpa-hairline, #eee); }
.dd-panel .dd-actions { display: flex; flex-wrap: wrap; gap: .5rem; }

/* AUTOMATIC COLLECTIONS V1 hardening: the dashboard's Direct Debit card --
   name + amount on one line, the plain-English reason wrapping beneath. */
.mpa-row.dd-attn-row { flex-wrap: wrap; row-gap: .15rem; }
.mpa-row.dd-attn-row .dd-attn-detail { flex: 1 1 100%; font-size: var(--mpa-text-sm); color: var(--mpa-muted); font-weight: 400; white-space: normal; }
/* Parent Direct Debit page, "Collections": on phones the status drops under
   the amount + pupil (the chevron stays on the first line). */
@media (max-width: 767.98px) {
  .mpa-row[data-dd-upcoming] { flex-wrap: wrap; row-gap: .1rem; }
  .mpa-row[data-dd-upcoming] .mpa-row-main { flex: 1 1 auto; }
  .mpa-row[data-dd-upcoming] .mpa-row-side { order: 3; flex: 1 1 100%; }
}
/* COMMUNICATIONS WORKSPACE V1 (19 Oct 2026) — /communications.
   Communications/Prospect polish (20 Oct 2026, Rory's acceptance): the
   history is ONE compact .mpa-table (sticky column headings from 992px, the
   house rule) instead of a card per email. Long addresses / subjects wrap
   inside their own cell and never widen the page. */
/* Round 2 (20 Oct 2026): Search / Show / Type / Period / From / To / Clear on ONE row from 1366px.
   Each field gets a preferred share and a real floor instead of the bar's flat 8.5rem + 1.1rem gap
   (which wrapped To and Clear onto a second row); the date boxes are as wide as a date. Weighted to
   (0,4,0) — the bar's own flex rule is (0,3,0), which silently beat the old search rule. Phones keep the
   house stack (one labelled row per field, Clear on the right). */
@media (min-width: 768px) {
  .mpa-filter-bar.mpa-comm-filters { column-gap: .8rem; }
  .mpa-filter-bar.mpa-comm-filters .mpa-filter-field.mpa-comm-f-search { flex: 1.5 1 10rem; min-width: 9.5rem; }
  .mpa-filter-bar.mpa-comm-filters .mpa-filter-field.mpa-comm-f-show { flex: 1 1 9rem; min-width: 8rem; }
  .mpa-filter-bar.mpa-comm-filters .mpa-filter-field.mpa-comm-f-type,
  .mpa-filter-bar.mpa-comm-filters .mpa-filter-field.mpa-comm-f-period { flex: 1 1 8rem; min-width: 7.5rem; }
  .mpa-filter-bar.mpa-comm-filters .mpa-filter-field.mpa-comm-f-date { flex: 0 0 auto; min-width: 0; }
  .mpa-filter-bar.mpa-comm-filters .mpa-filter-field.mpa-comm-f-date .form-control { width: 8.6rem !important; flex: 0 0 auto; }
}
/* Final corrections (21 Oct 2026): a date box's calendar button sits at the box's right edge — where every
   select's chevron sits — instead of trailing the text (where it moved with the value and the placeholder).
   Every From / To pair (Communications, Needs attention, Website requests). Pinned (absolute, vertically
   centred) so neither the value's length nor the field's display mode can move it. Chromium / Edge; Safari
   draws its own control. */
form[data-date-range] input[type="date"] { position: relative; padding-right: 2rem; }
form[data-date-range] input[type="date"]::-webkit-calendar-picker-indicator {
  position: absolute; right: .55rem; top: 50%; transform: translateY(-50%); margin: 0; cursor: pointer;
}
/* Phone: Needs attention is the page's primary control, as wide as the filter card under it (44px). */
@media (max-width: 767.98px) {
  .mpa-comm-head-actions > .btn { flex: 1 1 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; }
}
/* The attention banner: one compact line. "Show them" is part of the sentence's line box; its tap area is
   44px tall (padding) without making the line any taller (equal negative margin on an inline-block). */
.mpa-comm-addr { overflow-wrap: anywhere; word-break: break-word; }
.mpa-comm-subject { color: var(--mpa-ink); overflow-wrap: anywhere; }
.mpa-comm-sub { font-size: var(--mpa-text-sm); color: var(--mpa-muted); overflow-wrap: anywhere; line-height: 1.4; }
.mpa-comm-time { color: var(--mpa-muted); white-space: nowrap; }
.mpa-comm-tech > summary { cursor: pointer; }
.mpa-comm-links { display: flex; flex-direction: column; gap: .15rem; }
.mpa-comm-link { overflow-wrap: anywhere; font-weight: 600; font-size: var(--mpa-text-sm); }
a.mpa-comm-link { color: var(--mpa-ink-brand); text-decoration: none; }
a.mpa-comm-link:hover { text-decoration: underline; color: var(--mpa-ink-brand); }
.mpa-comm-link:not(a) { color: var(--mpa-muted); font-weight: 400; }
.mpa-comm-kind { color: var(--mpa-muted); font-weight: 400; }
.mpa-comm-c-type { font-weight: 600; }
.mpa-comm-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .15rem .75rem; margin-top: .15rem; }
.mpa-comm-actions .btn { padding: .1rem .5rem; }
.mpa-comm-issue { font-size: var(--mpa-text-sm); color: var(--mpa-ink); overflow-wrap: anywhere; line-height: 1.4; }
/* Final pass (20 Oct 2026). "Reviewed": a quiet note under the issue, never a new status — the pill keeps saying
   what actually happened. "Related to" with nothing to relate: a muted dash on desktop (no unexplained blank);
   a phone record simply has no such line. The label "Related to" introduces the links in a phone record only
   (the desktop column heading says it). The key under the list explains every status on any device. */
.mpa-comm-reviewed { font-size: var(--mpa-text-sm); color: var(--mpa-ok-ink); line-height: 1.35; margin-top: .1rem; }
.mpa-comm-none { color: var(--mpa-muted); }
.mpa-comm-rel-label { color: var(--mpa-muted); font-size: var(--mpa-text-sm); }
.mpa-comm-key > summary { cursor: pointer; color: var(--mpa-ink-brand); min-height: 44px; display: flex; align-items: center; }
.mpa-comm-key-list > div { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); gap: .15rem .75rem; padding: .3rem 0; border-top: 1px solid var(--mpa-hairline); }
.mpa-comm-key-list > div:first-child { border-top: 0; }
.mpa-comm-key-list dt { font-weight: 400; }
.mpa-comm-key-list dd { margin: 0; color: var(--mpa-ink); }
.mpa-comm-key-word { font-weight: 600; }
@media (min-width: 992px) {
  .mpa-comm-rel-label { display: none; }
}
@media (max-width: 575.98px) {
  .mpa-comm-key-list > div { grid-template-columns: minmax(0, 1fr); }
}
/* Final corrections (21 Oct 2026, Rory's desktop screenshot): the Status column is as wide as its widest
   pill ("May have been sent", nowrap ~9.3rem incl. cell padding) so no pill can spill into Issue, and a pill
   may never be wider than its cell (it wraps instead). On Needs attention the Issue column is the page's
   reason for being: it takes the share Subject and Recipient give up, and its actions (Try again / Mark
   reviewed / Technical details) sit on their own line under the sentence with a real gap. */
@media (min-width: 992px) {
  .mpa-comm-table.table { table-layout: fixed; }
  .mpa-comm-w-date { width: 7.25rem; }
  .mpa-comm-w-type { width: 9.5rem; }
  .mpa-comm-w-rcpt { width: 21%; }
  .mpa-comm-w-status { width: 10.5rem; }
  .mpa-comm-w-issue { width: 21%; }
  .mpa-comm-w-link { width: 10rem; }
  .mpa-comm-table.is-attention .mpa-comm-w-rcpt { width: 18%; }
  .mpa-comm-table.is-attention .mpa-comm-w-issue { width: 28%; }
  .mpa-comm-table.table tbody td { vertical-align: top; padding: .5rem .6rem; }
  .mpa-comm-table.table thead th { padding: .55rem .6rem; }
  .mpa-comm-c-date { font-size: var(--mpa-text-sm); }
  .mpa-comm-c-date .mpa-comm-time { display: block; }
  /* Round 2 (Rory): Type / Recipient / Subject / Status / Link centred, heading and cell alike; Date and
     Issue stay left (a date column and a sentence read best from the left). Desktop table only — the
     <992px records are never centred. */
  .mpa-comm-table.table :is(th, td):is(.mpa-comm-c-type, .mpa-comm-c-rcpt, .mpa-comm-c-subj, .mpa-comm-c-status, .mpa-comm-c-link) { text-align: center; }
  .mpa-comm-table .mpa-comm-links { align-items: center; }
  /* Final corrections: a pill never wider than its cell; the Issue actions on their own line, a real gap. */
  .mpa-comm-table .mpa-comm-c-status .mpa-chip { max-width: 100%; white-space: normal; text-align: center; }
  .mpa-comm-table .mpa-comm-c-issue .mpa-comm-actions { margin-top: .35rem; gap: .35rem .5rem; }
}

/* ── .mpa-rec-table: a desktop .mpa-table that becomes short RECORDS below
   992px (Communications/Prospect polish, 20 Oct 2026). Not .mpa-table-stack
   (one label:value line per cell — far too tall for a 7-11 column list) and
   never a squeezed / sideways-scrolling desktop table: each row is a small
   grid whose areas the table names itself (.mpa-comm-table,
   .pox-schools-table, .pox-plan-table). The base rules here undo the
   house <992px table behaviour (40rem floor, frozen first column, scroll
   wrapper) for these tables only. */
@media (max-width: 991.98px) {
  .table-responsive:has(> .mpa-rec-table) { overflow: visible; }
  .table-responsive:has(> .mpa-rec-table) > .mpa-table.table { min-width: 0; }
  .mpa-rec-table thead, .mpa-rec-table colgroup { display: none; }
  .mpa-rec-table, .mpa-rec-table > tbody { display: block; width: 100%; }
  .mpa-rec-table > tbody > tr { display: grid; gap: .2rem .75rem; padding: .7rem 1rem; border-top: 1px solid var(--mpa-hairline); align-items: start; }
  .mpa-rec-table > tbody > tr:first-child { border-top: 0; }
  .table-responsive:has(.mpa-table) .mpa-rec-table.table > tbody > tr > td,
  .mpa-rec-table.table > tbody > tr > td {
    display: block; padding: 0 !important; border: 0 !important; background: transparent !important;
    position: static !important; min-width: 0; white-space: normal; box-shadow: none !important;
  }
  /* Same weight as the cell rule above (+ .is-empty), so an empty cell really disappears: no empty grid track,
     no stray row-gap (ui_spacing_polish E2 measures the record's top/bottom symmetry). */
  .table-responsive:has(.mpa-table) .mpa-rec-table.table > tbody > tr > td.is-empty,
  .mpa-rec-table.table > tbody > tr > td.is-empty { display: none; }

  /* Line 1 = type + status (placed); every other cell takes a full-width row of its own in DOM order (date,
     recipient, subject, issue, link). Auto-placed, not named areas: an empty (.is-empty) cell then leaves no empty
     grid track behind, so no stray row-gap at the bottom of a record. */
  .mpa-comm-table > tbody > tr { grid-template-columns: minmax(0, 1fr) auto; }
  .mpa-comm-table > tbody > tr > td { grid-column: 1 / -1; }
  .mpa-comm-table > tbody > tr > td.mpa-comm-c-type { grid-column: 1; grid-row: 1; font-weight: 700; }
  .mpa-comm-table > tbody > tr > td.mpa-comm-c-status { grid-column: 2; grid-row: 1; justify-self: end; }
  .mpa-comm-c-date { font-size: var(--mpa-text-sm); color: var(--mpa-muted); }
  .mpa-comm-links { flex-direction: row; flex-wrap: wrap; gap: .1rem 1rem; }
  .mpa-comm-c-issue .btn, .mpa-comm-pager .btn { min-height: 44px; display: inline-flex; align-items: center; }
  /* Final pass: no related record = no line in a phone record (same weight as the .is-empty rule above). */
  .table-responsive:has(.mpa-table) .mpa-rec-table.table > tbody > tr > td.mpa-comm-c-link.is-none,
  .mpa-rec-table.table > tbody > tr > td.mpa-comm-c-link.is-none { display: none; }
}

/* Pupil page -> Parent portal access (Communications/Prospect polish, 20 Oct
   2026): ONE divider under "Email on record". The accounts list's first row
   loses its own hairline (it doubled the border right above it) and the rows
   line up with the text above instead of sitting a second inset deeper. */
.mpa-portal-accounts { padding-bottom: 0; }
.mpa-portal-accounts > .mpa-row:first-child { border-top: 0; }
.mpa-portal-accounts > .mpa-row { padding-left: 0; padding-right: 0; }

/* VAT INVOICING V1 (22 Oct 2026): the shared per-rate VAT summary table (parent VAT details). The admin
   VAT card's line list was removed 24 Oct 2026 — VAT is columns of the admin item table. */
.mpa-vat-summary { font-size: var(--mpa-text-sm); }
.mpa-vat-summary th { color: var(--mpa-muted); font-weight: 600; white-space: nowrap; }
.mpa-vat-summary td.text-end, .mpa-vat-summary th.text-end { white-space: nowrap; font-variant-numeric: tabular-nums; }
.mpa-vat-summary tfoot td { border-bottom: 0; }
.mpa-vat-supplier { overflow-wrap: anywhere; }
@media (max-width: 400px) {
  .mpa-vat-summary > :not(caption) > * > * { padding-left: .25rem; padding-right: .25rem; }
}
/* VAT INVOICING V1 presentation (23 Oct 2026): the parent's "View VAT
   breakdown" disclosure. Scoped here on purpose: the shared
   ".mpa-card > details > summary" rules near the "Native <details>/<summary>
   collapsible CARDS" note are swallowed by that note's comment (it ends with a
   template-style terminator, not a CSS one), so they never apply anywhere --
   a pre-existing issue left alone outside this change. */
#invoice-vat-card > details > summary { list-style: none; cursor: pointer; min-height: 44px; }
#invoice-vat-card > details > summary::-webkit-details-marker { display: none; }
#invoice-vat-card > details > summary:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: -2px; border-radius: var(--mpa-radius-sm); }
#invoice-vat-card > details[open] > summary .mpa-collapse-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: no-preference) {
  #invoice-vat-card .mpa-collapse-chevron { transition: transform .15s ease; }
}

/* VAT presentation pass (24 Oct 2026) ─────────────────────────────────────────────────────────────
   Settings → Accounting: the one VAT section. .set-prose-wide lets short explanations in these cards
   use the card's width (the Settings 75ch measure wrapped them at ~560px beside an empty card); they
   still wrap naturally on narrow screens. */
main.main-content:has(.settings-group-nav) .set-prose-wide .card-body :is(.form-text, p.text-muted, p.small) { max-width: none; }
.mpa-vat-form, .mpa-vat-setup, .mpa-vat-report { display: flex; flex-direction: column; gap: .75rem; }
.mpa-vat-setup[hidden] { display: none; }
.mpa-vat-setup > .row { margin-top: -.25rem; }
.mpa-vat-advanced > summary { cursor: pointer; font-weight: 600; width: fit-content; }
.mpa-vat-advanced > summary:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: 2px; border-radius: var(--mpa-radius-sm); }
.mpa-vat-advanced[open] > summary { margin-bottom: .5rem; }
/* VAT off with VAT invoices already published: their reports, compact and collapsed (24 Oct 2026). */
.mpa-vat-history { margin-top: 1rem; padding-top: .75rem; border-top: 1px solid var(--mpa-hairline); }
.mpa-vat-history > summary { cursor: pointer; font-weight: 600; width: fit-content; }
.mpa-vat-history > summary:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: 2px; border-radius: var(--mpa-radius-sm); }
.mpa-vat-history > .mpa-vat-report { margin-top: .75rem; max-width: 40rem; border: 0 !important; padding: 0 !important; height: auto; }
@media (min-width: 1200px) {
  .mpa-vat-report { border-left: 1px solid var(--mpa-line); padding-left: 1.25rem; height: 100%; }
}
@media (max-width: 1199.98px) {
  .mpa-vat-report { border-top: 1px solid var(--mpa-line); padding-top: 1rem; }
}

/* Field help (static/js/field_help.js, 24 Oct 2026): the explanation belongs to the LABEL — it shows
   on hover, while the field has keyboard focus, and on a phone when the label (or field) is tapped.
   No "?" symbol, no underline (Rory, acceptance), no permanent helper row. The field also points at
   the text with aria-describedby, so a screen reader reads it with the field. */
.mpa-fhelp { position: relative; }
.mpa-fhelp-label { cursor: help; }
.mpa-fhelp-pop { display: none; position: absolute; z-index: 30; left: 0; bottom: 100%;
  width: max-content; max-width: min(20rem, calc(100vw - 2rem)); padding: .5rem .65rem;
  background: var(--mpa-surface, #fff); color: var(--mpa-ink); border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm); box-shadow: 0 6px 18px rgba(20, 16, 40, .12);
  font-size: var(--mpa-text-sm); font-weight: 400; line-height: 1.4; white-space: normal; pointer-events: none; }
.mpa-fhelp[data-open] > .mpa-fhelp-pop { display: block; }

/* Class form: the VAT choice sits in the one row with type, price, duration and capacity. */
@media (min-width: 768px) {
  .mpa-cls-fields > .mpa-fw-vat { width: 17rem; }
  /* first row: type · price · VAT · duration · capacity (also the markup order) */
  .mpa-cls-fields > .mpa-fw-type { order: -5; }
  .mpa-cls-fields > .mpa-fw-price { order: -4; }
  .mpa-cls-fields > .mpa-fw-vat { order: -3; }
  .mpa-cls-fields > .mpa-fw-num:not(.mpa-fw-price) { order: -2; }
  .mpa-cls-fields > :first-child { order: -6; }
}
@media (min-width: 768px) and (max-width: 991.98px) {
  .mpa-cls-fields > .mpa-fw-vat .form-label { min-height: 2.9em; align-items: flex-end; }
}
@media (max-width: 767.98px) {
  /* phones: VAT below the three numbers (price · duration · capacity share one row), before the venue grid */
  .mpa-cls-fields > .mpa-fw-vat { order: 1; }
  .mpa-cls-fields > .col-12:has(> .mpa-cls-place) { order: 2; }
  /* three numbers share a phone row: room for "No limit" without shrinking the 16px text */
  .mpa-cls-fields > .mpa-fw-num .form-control { padding-left: .55rem; padding-right: .55rem; }
}

/* Admin invoice — ONE item table for a VAT invoice (.mpa-invoice-lines--vat, 24 Oct 2026):
   Description | Price incl. VAT | Qty | Net | VAT | VAT amount | Total | delete. The shared
   .mpa-invoice-lines rules address the total as td:nth-child(4); in this variant that column is
   Net, so the total's colour/weight rules are re-pointed at .mpa-li-total here. Non-VAT invoices
   keep the original table untouched. */
.mpa-th-sub { display: block; font-weight: 400; font-size: var(--mpa-text-2xs); color: var(--mpa-muted); text-transform: none; letter-spacing: 0; }
.mpa-invoice-lines--vat .mpa-li-k { display: none; }
.mpa-invoice-lines--vat tbody td:is(.mpa-li-net, .mpa-li-vat, .mpa-li-vatamt, .mpa-li-total) { font-variant-numeric: tabular-nums; white-space: nowrap; }
.mpa-invoice-lines--vat tbody tr:is(.table-warning, .table-success) td:nth-child(4) { color: inherit; font-weight: 400; }
.mpa-invoice-lines--vat tbody tr.table-warning td.mpa-li-total { color: var(--mpa-warn-ink); font-weight: 600; }
.mpa-invoice-lines--vat tbody tr.table-success td.mpa-li-total { color: var(--mpa-ok-ink); font-weight: 600; }
.mpa-invoice-lines--vat tbody td.mpa-li-vat[data-vat-kind="none"] { color: var(--mpa-muted); }
.mpa-invoice-lines--vat tbody td.mpa-li-total input[type="number"] { text-align: right !important; }
.mpa-invoice-lines--vat tfoot td.text-end { font-variant-numeric: tabular-nums; white-space: nowrap; }
.mpa-invoice-lines--vat tfoot tr.mpa-vat-sum-row td { padding-top: .2rem; padding-bottom: .2rem; border-bottom: 0; }
.mpa-invoice-lines--vat tfoot tr.mpa-vat-sum-row:first-child td { padding-top: .5rem; }
.mpa-invoice-lines--vat tfoot tr.mpa-vat-total-row td { border-top: 1px solid var(--mpa-line); padding-top: .45rem; padding-bottom: .45rem; }
@media (min-width: 576px) and (max-width: 991.98px) {
  /* tablets: the eight columns size to their content instead of fixed shares */
  .mpa-invoice-lines--vat { table-layout: auto !important; }
  .mpa-invoice-lines--vat thead th { white-space: nowrap; }
  .mpa-invoice-lines--vat tbody td:first-child { min-width: 8rem; }
}
@media (max-width: 575.98px) {
  /* Phones: three lines per item — the description; Price × Qty (editable, side by side) with the
     total; then labelled Net and VAT. Same cells and inputs as desktop; layout only. */
  .mpa-invoice-lines--vat tbody tr:not(:has(> td[colspan])) {
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr) auto var(--inv-rail);
    column-gap: .35rem; row-gap: .2rem; align-items: center;
  }
  .mpa-invoice-lines--vat tbody td:first-child { grid-column: 1 / 5; grid-row: 1; }
  .mpa-invoice-lines--vat tbody td:nth-child(2) { grid-column: 1; grid-row: 2; }
  .mpa-invoice-lines--vat tbody td:nth-child(3) { grid-column: 2; grid-row: 2; }
  .mpa-invoice-lines--vat tbody td.mpa-li-total {
    grid-column: 4; grid-row: 2; justify-self: end; text-align: right;
    font-weight: 600; font-size: var(--mpa-text-base) !important;
  }
  .mpa-invoice-lines--vat tbody td:last-child { grid-column: 5; grid-row: 1 / 3; }
  .mpa-invoice-lines--vat tbody td:nth-child(4).mpa-li-net {
    grid-column: 1 / 3; grid-row: 3; margin-left: 0; text-align: left;
    font-weight: 400; font-size: var(--mpa-text-sm) !important; color: var(--mpa-muted);
  }
  .mpa-invoice-lines--vat tbody td.mpa-li-vat {
    grid-column: 3 / 5; grid-row: 3; text-align: right;
    font-size: var(--mpa-text-sm) !important; color: var(--mpa-muted);
  }
  .mpa-invoice-lines--vat tbody td.mpa-li-vatamt { display: none !important; }
  .mpa-invoice-lines--vat .mpa-li-k { display: inline; }
  /* an editable amount (one-off line) on its total cell, as the original table does on col 4 */
  .mpa-invoice-lines--vat tbody td.mpa-li-total input[type="number"] {
    display: inline-block; width: 5rem !important; font-size: var(--mpa-text-base) !important; font-weight: 600;
  }
  .mpa-invoice-lines--vat tbody td.mpa-li-total:has(> input)::before { content: "£"; font-weight: 600; }
  /* Rate-less rows (discount, fee, one-off item): description and amount share line 1. */
  .mpa-invoice-lines--vat tbody tr:has(td.mpa-li-blank) td:first-child { grid-column: 1 / 4; }
  .mpa-invoice-lines--vat tbody tr:has(td.mpa-li-blank) td.mpa-li-total { grid-row: 1; }
  .mpa-invoice-lines--vat tbody tr:has(td.mpa-li-blank) td:last-child { grid-row: 1 / 3; }
  .mpa-invoice-lines--vat tbody tr:has(td.mpa-li-blank) :is(td.mpa-li-net, td.mpa-li-vat) { grid-row: 2; }
  .mpa-inv-msrow.mpa-inv-msvat { font-size: var(--mpa-text-sm); }
  .mpa-inv-msrow.mpa-inv-msvat > span:last-child { font-weight: 500; }
}

/* VAT table polish: Net is an ordinary figure (the shared 4th-column rules made it bold/large); the
   line Total carries the weight. Phone alignment beats Bootstrap's !important text utilities. */
.mpa-invoice-lines--vat tbody td.mpa-li-net { font-weight: 400; }
.mpa-invoice-lines--vat tbody td.mpa-li-total { font-weight: 600; }
.mpa-invoice-lines--vat tbody td:is(.mpa-li-net, .mpa-li-vatamt, .mpa-li-vat) { color: var(--mpa-ink); }
@media (max-width: 575.98px) {
  .mpa-invoice-lines--vat tbody td:nth-child(4).mpa-li-net { text-align: left !important; justify-self: start; color: var(--mpa-muted); }
  .mpa-invoice-lines--vat tbody td.mpa-li-vat { text-align: right !important; justify-self: end; color: var(--mpa-muted); }
}

/* VAT breakdown on demand (24 Oct 2026, Rory): by default the VAT table shows Description · Price ·
   Quantity · VAT treatment · Total; "Show VAT breakdown" reveals Net and VAT amount (.mpa-vx) in the
   same table, the same phone cards and the phone summary. */
[data-vat-lines-card]:not(.is-vat-open) .mpa-vx { display: none !important; }
.mpa-vat-toggle { white-space: nowrap; }
.mpa-li-split { margin-top: .1rem; font-weight: 400; white-space: normal; }
.mpa-vat-after { white-space: nowrap; }
.mpa-invoice-lines--vat tfoot tr.mpa-vat-recon-row td { padding-top: .2rem; padding-bottom: .2rem; border-bottom: 0; }
.mpa-invoice-lines--vat tfoot tr.mpa-vat-recon-row:first-child td { padding-top: .5rem; }
.mpa-invoice-lines--vat tfoot tr.mpa-vat-recon-row + tr.mpa-vat-sum-row td { border-top: 1px solid var(--mpa-hairline); padding-top: .4rem; }
.mpa-inv-msrow.mpa-inv-mssub { padding-top: 0; color: var(--mpa-muted); }
/* One computed size for the main item values (24 Oct 2026): read-only Price and Quantity were
   .855rem beside 1rem VAT and Total. Phones: the item's price × quantity line matches its total;
   editable inputs keep their 16px (no iOS focus zoom). */
@media (min-width: 576px) {
  .mpa-invoice-lines--vat tbody td:is(:nth-child(2), :nth-child(3)) { font-size: 1rem; }
}
@media (max-width: 575.98px) {
  .mpa-invoice-lines--vat tbody td:is(:nth-child(2), :nth-child(3)):not(.mpa-li-rate-edit) { font-size: var(--mpa-text-base) !important; }
}
@media (max-width: 575.98px) {
  /* collapsed: the VAT treatment takes the whole third line of each card */
  [data-vat-lines-card]:not(.is-vat-open) .mpa-invoice-lines--vat tbody td.mpa-li-vat {
    grid-column: 1 / 5; text-align: left !important; justify-self: start;
  }
}

/* Invoice note + Save (admin invoice): note grows up to a readable width, the button keeps its own
   size, both sit on one bottom edge; stacked on phones. */
.mpa-inv-notesave { display: flex; align-items: flex-end; gap: .75rem; flex-wrap: wrap; }
.mpa-inv-note { flex: 1 1 22rem; max-width: 46rem; min-width: 0; }
.mpa-inv-note textarea { min-height: calc(1.5em + .5rem + 2px); resize: vertical; }
.mpa-inv-save { flex: 0 0 auto; }
@media (max-width: 575.98px) {
  .mpa-inv-note { flex-basis: 100%; }
  .mpa-inv-save { width: 100%; }
}
@media (min-width: 576px) {
  /* one readable size for every figure in the VAT table; only the line Total is emphasised */
  .mpa-invoice-lines--vat tbody td:is(.mpa-li-net, .mpa-li-vatamt, .mpa-li-total, .mpa-li-vat),
  .mpa-invoice-lines--vat tbody td:is(.mpa-li-net, .mpa-li-vatamt, .mpa-li-total) span { font-size: 1rem; }
  .mpa-invoice-lines--vat tbody td:is(.mpa-li-net, .mpa-li-vatamt) span { font-weight: 400; }
  .mpa-invoice-lines--vat tbody td.mpa-li-total span,
  .mpa-invoice-lines--vat tbody td.mpa-li-total input[type="number"] { font-weight: 600; font-size: 1rem; }
}
@media (min-width: 768px) {
  /* one-off row: the product picker matches the row's small controls so every input shares a baseline */
  .mpa-oneoff-form .form-select-sm.mpa-select { min-height: 0; padding-top: .25rem; padding-bottom: .25rem; font-size: .875rem; line-height: 1.5; }
}

/* Parent invoice, compact layout (24 Oct 2026, second walkthrough): a centred invoice — issuer and
   identity, one summary of the amount due, the charges beside "How to pay" from 992px (stacked
   below), the PDF actions as one row of two equal buttons. Solis cards, chips, tabs and buttons
   throughout; the shared portal heading above it is unchanged. */
.mpa-pinv-doc { max-width: 62rem; margin-inline: auto; }
.mpa-pinv-nav { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem; margin-bottom: .75rem; }
.mpa-pinv-top { display: grid; grid-template-columns: minmax(0, 1fr); gap: .75rem 2rem; }
@media (min-width: 768px) { .mpa-pinv-top { grid-template-columns: minmax(0, 1fr) auto; } }
.mpa-pinv-school { min-width: 0; overflow-wrap: anywhere; }
.mpa-pinv-idfacts { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: .75rem; row-gap: .2rem;
  margin: 0; font-size: var(--mpa-text-md); align-content: start; }
.mpa-pinv-idfacts > div { display: contents; }
.mpa-pinv-idfacts dt { font-weight: 400; color: var(--mpa-muted); }
.mpa-pinv-idfacts dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
@media (min-width: 768px) {
  .mpa-pinv-top > .mpa-pinv-idfacts dt { text-align: right; }
  .mpa-pinv-top > .mpa-pinv-idfacts { max-width: 26rem; }
}
.mpa-pinv-summary { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem 1rem;
  margin-top: 1rem; padding: .9rem 1rem; background: var(--mpa-bg); border: 1px solid var(--mpa-line);
  border-radius: var(--mpa-radius-sm); }
.mpa-pinv-sumfig { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.mpa-pinv-sumfig .mpa-stat-label { margin-top: 0; }
.mpa-pinv-sumfig .mpa-stat-note { margin-top: 0; }
.mpa-pinv-status { display: flex; flex-wrap: wrap; gap: .35rem; }
.mpa-pinv-grid { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: 1rem; }
@media (min-width: 992px) { .mpa-pinv-grid { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); align-items: start; } }
.mpa-pinv-aside { min-width: 0; }
.mpa-pinv-item { display: block; font-weight: 600; }
.mpa-pinv-lines .mpa-row .mpa-pinv-itemnote { display: block; margin-top: .1rem; }
.mpa-pinv-lines .mpa-row > .mpa-row-side:not(.mpa-text-ok) { color: var(--mpa-ink); }
.mpa-pinv-vat { border-top: 1px solid var(--mpa-hairline); }
.mpa-pinv-vat > details > summary { display: flex; align-items: center; gap: .4rem; list-style: none; cursor: pointer;
  padding: .7rem 1rem; min-height: 44px; font-weight: 600; font-size: var(--mpa-text-sm); color: var(--mpa-ink); }
.mpa-pinv-vat > details > summary::-webkit-details-marker { display: none; }
.mpa-pinv-vat > details > summary:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: -2px; border-radius: var(--mpa-radius-sm); }
.mpa-pinv-vat > details[open] > summary .mpa-collapse-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: no-preference) { .mpa-pinv-vat .mpa-collapse-chevron { transition: transform .15s ease; } }
.mpa-pinv-vat-body { padding: 0 1rem .9rem; }
/* How to pay: the shared segmented control, filling the panel as two equal tabs. */
.mpa-tabs.mpa-pinv-paytabs { display: grid; grid-template-columns: 1fr 1fr; width: 100%; margin-bottom: 1rem; }
.mpa-pinv-paytabs .nav-link { text-align: center; font-size: var(--mpa-text-base); padding: .55rem .5rem; min-height: 44px; }
.mpa-pinv-paytabs .nav-link:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: 1px; }
.mpa-pinv-paypanel:focus-visible { outline: 2px solid var(--mpa-action); outline-offset: 4px; border-radius: var(--mpa-radius-sm); }
.mpa-pinv-paytitle { font-weight: 700; margin-bottom: .5rem; display: flex; align-items: center; gap: .4rem; }
.mpa-pinv-payamount { display: flex; flex-direction: column; gap: .1rem; margin-bottom: .5rem; }
.mpa-pinv-payamount .mpa-stat-label { margin-top: 0; }
.mpa-pinv-paynote { font-size: var(--mpa-text-sm); color: var(--mpa-muted); margin: .5rem 0 .75rem; }
.mpa-pinv-paypanel .btn { min-height: 44px; }
/* Bank details: read-only facts, never boxes that look like inputs. The account name has its own
   full row (label above, both left aligned, long names wrap); sort code and account number share one;
   the reference is the largest value, selectable in one tap and copyable. */
.mpa-pinv-bankd { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem 1rem; margin: .75rem 0 0;
  padding-top: .75rem; border-top: 1px solid var(--mpa-hairline); }
.mpa-pinv-bankd > div { min-width: 0; }
.mpa-pinv-bankd > .full { grid-column: 1 / -1; }
.mpa-pinv-bankd dt { font-size: var(--mpa-text-xs); font-weight: 700; letter-spacing: var(--mpa-tracking-label);
  text-transform: uppercase; color: var(--mpa-muted); }
.mpa-pinv-bankd dd { margin: .15rem 0 0; font-weight: 600; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.mpa-pinv-ref dd { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .75rem; }
.mpa-pinv-refv { font-size: var(--mpa-text-lg); font-weight: 800; -webkit-user-select: all; user-select: all; }
.mpa-action-btn.mpa-pinv-copy { padding: .3rem .7rem; box-shadow: none; font-size: var(--mpa-text-sm); min-height: 36px; }
/* View / Download invoice: one row, two equal halves, matching height. */
.mpa-pinv-docactions { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; margin-bottom: 1rem; }
.mpa-pinv-docactions > .mpa-action-btn { justify-content: center; min-width: 0; min-height: 44px; padding: .55rem .75rem; text-align: center; }
@media (max-width: 399.98px) {
  /* 360px phones: both labels stay on one line */
  .mpa-pinv-docactions > .mpa-action-btn { padding-inline: .4rem; gap: .35rem; white-space: nowrap; }
}

/* Parent homepage contact line: label and address share a baseline; wraps naturally on phones. */
.mpa-contact-inline { overflow-wrap: anywhere; }
.mpa-contact-inline .mpa-link { margin-left: 0; white-space: normal; text-decoration: underline; text-underline-offset: 2px; }

.mpa-pinv-nav > .mpa-link { margin-left: 0; margin-right: auto; }
.mpa-pinv-school > .mpa-stat-note { margin: .1rem 0 0; padding: 0; display: block; }

@media (max-width: 575.98px) {
  /* Price and quantity editors on phones: one computed size, 16px — iOS zooms the whole page when a
     smaller input is focused, which made the compact editors hard to use (measured 13.68px). */
  .mpa-invoice-lines tbody td.mpa-li-rate-edit input[type="number"] { font-size: 16px !important; }
}

/* Parent homepage Invoices card: from 992px the desktop density rule gives card heads 1rem side padding
   while rows keep 1.25rem, so each row's status chip ended ~3px short of "View all". The rows here use
   the head's padding, so chip and link share one right edge (and names line up under the title). */
@media (min-width: 992px) {
  .mpa-pdash-invoices > .mpa-rows > .mpa-row { padding-left: 1rem; padding-right: 1rem; }
}
@media (max-width: 575.98px) {
  /* "Net £100.00" and "Exempt · no VAT" each stay on one line (the shared phone rule wraps cells) */
  .mpa-invoice-lines--vat tbody td:is(.mpa-li-net, .mpa-li-vat) { white-space: nowrap !important; }
}
/* The address moves to its own line as a whole when it doesn't fit; it only breaks inside as a last resort. */
.mpa-contact-inline .mpa-link { display: inline-block; max-width: 100%; overflow-wrap: anywhere; }
@media (max-width: 575.98px) {
  /* the VAT invoice number / tax point take their own line under "Line items" on phones */
  .mpa-li-vatid { flex-basis: 100%; margin-left: 0 !important; text-align: left !important; }
}

/* ── UI/UX pass 1 (25 Oct 2026) — layout and accessibility fixes from the
   24 Oct review (docs/acceptance/ui_ux_pass_1/HANDOVER.md). Shared tokens
   and components only; no new colours. ── */
/* Install bar (base.html): while it shows, the page reserves its height so
   the last rows, buttons and footer links can be scrolled clear of it. */
body.mpa-install-showing { padding-bottom: var(--mpa-install-h, 0px); }
body.mpa-install-showing .mpa-back-to-top { bottom: calc(var(--mpa-install-h, 0px) + 1rem); }
/* A row's meta line that may wrap instead of running past its card
   (teacher dashboard Upcoming events, with .mpa-row-stack). */
.mpa-row-side.wrap { white-space: normal; flex-wrap: wrap; row-gap: .1rem; }
/* Enquiries between 768px and 1199px: the phone cards, two to a row
   (classes/interest.html) instead of a table scrolling inside its card. */
@media (min-width: 768px) and (max-width: 1199.98px) {
  /* Revision 5: the details on the left, the main action at the upper right
     and Actions at the lower right, Actions' bottom level with the last
     details line. Every pair of cards owns four grid tracks: A1 (main
     action), A2 (the rest of the details, Actions), B (whatever the card
     opens: its Actions menu or an inline panel) and a fixed gap. A closed
     card spans A1-A2, so the two cards in a row match each other's height
     (and only each other's). A card with something open spans A1-B: only
     that card's box grows, its neighbour keeps its closed height with its
     Actions where it was, and the next row starts below the open card.
     --enq-row (classes/interest.html) is the pair's first track. Both card
     and body are subgrids, so a card lays out the same way open or closed
     (the size container is the grid, not the card: containment would turn
     the card's subgrid off). */
  .mpa-enq-mgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: auto auto auto .6rem;
    column-gap: .6rem; row-gap: 0; align-items: stretch; margin-bottom: -.6rem; container: enqgrid / inline-size; }
  .mpa-enq-mgrid > .mpa-enq-mcard { margin-bottom: 0 !important;
    grid-row: var(--enq-row, auto) / span 2; display: grid; grid-template-rows: subgrid; grid-template-columns: minmax(0, 1fr);
    /* the bottom edge is painted, not a border: a subgrid's border counts
       towards its LAST track, which would move by 1px when the card opens */
    border-bottom-width: 0; box-shadow: inset 0 -1px 0 var(--mpa-line), var(--mpa-shadow); }
  .mpa-enq-mgrid > .mpa-enq-mcard:has(.dropdown-menu.show, .mpa-enq-tpanel.show, .mpa-enq-tpanel.collapsing) { grid-row: var(--enq-row, auto) / span 3; }
  .mpa-enq-mgrid .mpa-enq-mbody { grid-row: 1 / -1; grid-column: 1; display: grid; grid-template-rows: subgrid;
    grid-template-columns: minmax(0, 1fr) clamp(6rem, calc(50cqi - 16rem), 8rem); column-gap: .9rem; padding-bottom: 0 !important; }
  .mpa-enq-mgrid .mpa-enq-m-info { grid-column: 1; grid-row: 1 / 3; min-width: 0; margin-bottom: .7rem; }
  .mpa-enq-mgrid .mpa-enq-tcol, .mpa-enq-mgrid .mpa-enq-tcol .mpa-enq-tactions,
  .mpa-enq-mgrid .mpa-enq-tcol .dropdown { display: contents !important; }
  .mpa-enq-mgrid .mpa-enq-tactions > form, .mpa-enq-mgrid .mpa-enq-tactions > a { grid-column: 2; grid-row: 1; align-self: start; }
  .mpa-enq-mgrid .mpa-enq-tactions [data-enq-actions-menu] { grid-column: 2; grid-row: 2; align-self: end; margin: .45rem 0 .7rem; }
  .mpa-enq-mgrid .mpa-enq-tactions > form .btn, .mpa-enq-mgrid .mpa-enq-tactions > a,
  .mpa-enq-mgrid .mpa-enq-tactions [data-enq-actions-menu] { width: 100%; justify-content: center; white-space: normal; text-align: center; line-height: 1.2; }
  /* The open menu sits in the card, under Actions (no floating layer over
     the next card); Escape, arrow keys and a click outside work as before. */
  .mpa-enq-mgrid .mpa-enq-tactions .dropdown-menu { grid-column: 1 / -1; grid-row: 3; justify-self: end; position: static !important;
    inset: auto !important; transform: none !important; margin: 0 0 .7rem !important; }
  /* align-self: end -- with nothing open it is an empty, zero-height box (a
     closed card has no track B, so it lands on A2) and never covers Actions. */
  .mpa-enq-mgrid .mpa-enq-tpanels { grid-column: 1 / -1; grid-row: 3; align-self: end; margin-bottom: .7rem; min-width: 0; }
  .mpa-enq-mgrid .mpa-enq-tpanels > .mpa-enq-tpanel > form { margin-top: 0 !important; }
}
/* The action column is 8rem wherever a card has room for it (991px up) and
   narrows with the card below that -- to 96px at 768 -- leaving the details
   ~213px; their longest line (an email address) needs 208px, so nothing is
   squeezed; a long button label wraps inside its button. Narrow cards
   (a grid under 810px) also use a slightly smaller gap and button padding. */
@container enqgrid (max-width: 809.98px) {
  .mpa-enq-mgrid .mpa-enq-mbody { column-gap: .75rem; }
  .mpa-enq-mgrid .mpa-enq-tactions .btn { padding-left: .5rem; padding-right: .5rem; }
}
/* Timetable class drawer roster (timetable/panel.html): a labelled trial
   conversion, a labelled-icon date choice, the shared kebab for the rest.
   Targets in px: the desktop root font is smaller, so 2rem was only 26px. */
.mpa-roster-primary { min-height: 32px; padding: .2rem .6rem; font-size: .8rem; font-weight: 600; white-space: nowrap; }
/* The purple Enrol treatment (revision 5: one definition, as Bootstrap
   button variables so hover, focus, pressed and disabled all follow it):
   the roster's Enrol, and the enrolment confirmation's main action --
   Enrol {pupil}, Choose start date, Convert pupil. */
.mpa-roster-primary, .mpa-btn-enrol {
  --bs-btn-color: var(--mpa-trial-ink); --bs-btn-bg: var(--mpa-trial-bg); --bs-btn-border-color: transparent;
  --bs-btn-hover-color: var(--mpa-trial-ink); --bs-btn-hover-bg: var(--mpa-trial-bg); --bs-btn-hover-border-color: var(--mpa-trial);
  --bs-btn-active-color: var(--mpa-trial-ink); --bs-btn-active-bg: var(--mpa-trial-bg); --bs-btn-active-border-color: var(--mpa-trial);
  --bs-btn-disabled-color: var(--mpa-trial-ink); --bs-btn-disabled-bg: var(--mpa-trial-bg); --bs-btn-disabled-border-color: transparent;
  --bs-btn-focus-shadow-rgb: 111, 66, 193; }
.mpa-btn-enrol { font-weight: 600; }
/* Revision 8: on touch screens the drawer's purple enrolment buttons (Add
   pupil "Enrol pupil", the picker's "Convert pupil") are REAL 44px targets
   -- the button itself, no invisible extension (those failed bottom-edge
   hit tests in revision 3). The confirmation's buttons already are. */
@media (pointer: coarse) {
  #slot-drawer .btn.mpa-btn-enrol { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
}
.mpa-roster-icon { min-width: 32px; min-height: 32px; padding: .2rem .4rem; color: var(--mpa-muted); }
.mpa-roster-undo { min-height: 32px; padding: .2rem .5rem; font-size: .8rem; }
.mpa-roster-kebab .btn { min-width: 32px; min-height: 32px; }
/* Revision 3: the actions (Enrol for a trial, then the kebab) are ONE group
   in a fixed right-hand column on every row; the name cell takes the rest
   and wraps inside itself -- the date / Trial chips drop under the name
   when they don't fit, and the name is never truncated. The Enrol
   confirmation opens as a full-width last line of the row. */
.mpa-roster-row { flex-wrap: wrap; column-gap: .25rem; }
.mpa-roster-row > .mpa-roster-name { flex: 1 1 0; min-width: 0; overflow-wrap: break-word; line-height: 1.35; }
.mpa-roster-row > .mpa-roster-name .mpa-chip { vertical-align: middle; }
.mpa-roster-confirm { flex: 0 0 100%; }
.mpa-roster-confirm-body { border-top: 1px solid var(--mpa-line); margin-top: .35rem; padding: .5rem 0 .4rem; font-size: .85rem; }
.mpa-roster-confirm-title { font-weight: 600; }
/* Revision 5: the confirmation's buttons fill the row -- Enrol {pupil} and
   Change start date share the space evenly (never below their own label
   width), Cancel keeps its content width at the far right. When they cannot
   all fit, Cancel (then Change start date) wraps to the next row, still
   full-size, Cancel still at the right. */
.mpa-roster-confirm-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
.mpa-roster-confirm-actions > .btn { flex: 1 1 0; min-width: max-content; justify-content: center; text-align: center; }
.mpa-roster-confirm-actions > .mpa-roster-cancel { flex: 0 0 auto; min-width: 0; margin-left: auto; }
@media (pointer: coarse) {
  /* Compact phone rows (revision 3): every roster control is a REAL 44x44px
     target (no invisible extension), and the row has no vertical padding of
     its own -- an ordinary pupil is one 44px line whose control fills it, so
     no tap area reaches into the pupil above or below. */
  .mpa-roster-primary, .mpa-roster-icon, .mpa-roster-undo, .mpa-roster-kebab .btn { min-height: 44px; min-width: 44px; }
  /* 46px rows: the 44px control is centred with 1px above and below, so two
     pupils' targets are always at least 3px apart (incl. the row border). */
  .mpa-roster-row { padding-top: 0 !important; padding-bottom: 0 !important; min-height: 46px; }
}
/* Success-green TEXT reads at 5.7:1 (ok-ink); the brighter --mpa-ok stays
   for meters, bars and icons. Bootstrap's .text-success was 4.32:1 on the
   tinted invoice rows. */
.text-success { color: var(--mpa-ok-ink) !important; }
/* Invoice list rows (invoices/_results.html): View + the labelled row menu
   that now holds Void…; on the phone card (< 576px, the card layout's own
   breakpoint) View keeps the width and the menu button is a 44px square. */
.mpa-inv-row-actions { display: inline-flex; align-items: center; gap: .25rem; }
.mpa-inv-menu-spacer { visibility: hidden; pointer-events: none; }
@media (max-width: 575.98px) {
  .mpa-inv-list tbody td.inv-c-actions .mpa-inv-row-actions { display: flex; flex: 1 1 auto; gap: .5rem; }
  .mpa-inv-list tbody td.inv-c-actions .mpa-inv-row-actions > .btn { flex: 1 1 0; }
  .mpa-inv-list tbody td.inv-c-actions .dropdown > .btn { flex: 0 0 auto; min-width: 44px; min-height: 38px; }
  .mpa-inv-list tbody td.inv-c-actions .mpa-inv-menu-spacer { display: none !important; }
}
/* Revision 5: this pass put the row menu beside View on the phone card, so
   that action row's two controls are real 44px targets on touch screens
   (they were 38px); same links, same menu. */
@media (max-width: 575.98px) and (pointer: coarse) {
  .mpa-inv-list tbody td.inv-c-actions .mpa-inv-row-actions > .btn,
  .mpa-inv-list tbody td.inv-c-actions .dropdown > .btn { min-height: 44px; }
}
/* Phone filter toolbar (invoices/index.html, < 768px; revision 2): a full-width
   Filters button (house .mpa-btn-nav) with a chevron that turns when open,
   and Clear beside it while filters are on. */
.mpa-inv-filterbar { display: flex; gap: .5rem; align-items: stretch; margin: 0 0 .5rem; }
.mpa-inv-filter-btn { flex: 1 1 auto; display: inline-flex; align-items: center; gap: .45rem; min-height: 44px; padding: 0 .85rem; }
.mpa-inv-filterbar .btn { min-height: 44px; display: inline-flex; align-items: center; }
.mpa-inv-filter-chev { margin-left: auto; transition: transform .15s ease; }
.mpa-inv-filter-btn[aria-expanded="true"] .mpa-inv-filter-chev { transform: rotate(180deg); }
@media (max-width: 767.98px) { #invoice-filters-wrap > .mpa-filter-bar { margin-bottom: .5rem; } }
/* Invoice "Invoice details" line (invoices/detail.html): each "Label date"
   stays whole; whole items wrap at narrow widths (revision 2, 25 Oct 2026). */
.mpa-meta-item { white-space: nowrap; }
.mpa-meta-sep { margin: 0 .2rem 0 .1rem; }
/* Tablet enquiry card actions (classes/interest.html row_actions_menu,
   768-1199px; revision 2): one main action + a labelled Actions menu; the
   menu's inline panels (class / close / delete) span the card below. */
.mpa-enq-tactions { display: flex; align-items: center; gap: .5rem; margin-top: .6rem; }
.mpa-enq-tactions .btn { min-height: 40px; display: inline-flex; align-items: center; gap: .35rem; padding: 0 .9rem; }
.mpa-enq-tpanel > form { margin-top: .5rem !important; }
/* Tablet Students table (students/_results.html + _row_tablet.html, admin,
   768-991px; revision 2): four columns that fit, the house table type size,
   contact text wrapping, the rest of the row in an expandable details row. */
.mpa-stu-ttable { table-layout: fixed; width: 100%; }
.mpa-stu-ttable td { vertical-align: top; }
.mpa-stu-sub { color: var(--mpa-muted); font-size: var(--mpa-text-sm); line-height: 1.35; }
.mpa-stu-contact, .mpa-stu-pupil { overflow-wrap: break-word; }
.mpa-stu-ttable td.text-center, .mpa-stu-actions { vertical-align: middle; }
/* Breaks after the @ first (wbr_email), then anywhere only if one half still can't fit. */
.mpa-stu-email { overflow-wrap: anywhere; }
.mpa-stu-actions { white-space: nowrap; }
.mpa-stu-actions > * { display: inline-flex; vertical-align: middle; }
.mpa-stu-actions .btn { min-height: 36px; align-items: center; gap: .3rem; }
.mpa-stu-expand[aria-expanded="true"] .bi-chevron-down { transform: rotate(180deg); }
.mpa-stu-more > td { background: var(--mpa-tint); border-top: 0; }
/* Revision 5: Birthday, Gender and Date added take only their values' width;
   Siblings takes the rest, out to the row's right edge. */
.mpa-stu-more-grid { display: grid; grid-template-columns: max-content max-content max-content minmax(0, 1fr); gap: .5rem 1.75rem; margin: 0; }
.mpa-stu-more-grid dt { font-size: var(--mpa-text-xs, .72rem); text-transform: uppercase; letter-spacing: .04em; color: var(--mpa-muted); font-weight: 600; }
.mpa-stu-more-grid dd { margin: 0; overflow-wrap: anywhere; }
/* Invoice list (invoices/_results.html; revision 3): ONE lifecycle-chip copy
   per width -- the Status cell from 576px up, beside the name on the phone
   card. Revision 6, phone card: the name cell spans the card's full width
   under the total; an invisible copy of the total (.mpa-inv-amt-sp) floated
   right keeps only the name's FIRST line clear of the real total, so a
   wrapped name and the badges below it use the whole width. The badges are
   ONE group (.mpa-inv-life-m: status, then DD / refund / Overdue) that
   follows the name's last line when the whole group fits there, otherwise
   moves down whole, and only wraps inside itself if it is wider than the
   card. Text, not boxes, lines up: the group sits on the name's baseline,
   and a group that starts a line is pulled left by the badge's own padding
   so its TEXT starts on the name's left edge (the coloured background uses
   the gutter); the spacer before it carries that padding plus the gap, and
   disappears when the group wraps. */
.mpa-inv-life { display: inline-flex; flex-wrap: wrap; align-items: center; gap: .3rem; }
.mpa-inv-life:empty { display: none; }
.mpa-inv-life-m, .mpa-inv-life-sp, .mpa-inv-amt-sp { display: none; }
@media (max-width: 575.98px) {
  .mpa-inv-life-d { display: none; }
  .mpa-inv-list tbody td.inv-c-name { grid-column: 2 / -1; display: block; overflow-wrap: break-word; }
  .mpa-inv-list tbody td.inv-c-total { grid-column: 3; }
  .mpa-inv-list tbody td.inv-c-name > .mpa-inv-amt-sp {
    display: block; float: right; visibility: hidden; margin-left: .19rem;
    font-weight: 700; font-size: var(--mpa-text-base); font-variant-numeric: tabular-nums; white-space: nowrap; }
  .mpa-inv-list tbody td.inv-c-name > .mpa-inv-life-sp { display: inline; font-size: 0; word-spacing: .82rem; }  /* .6rem pull-back + ~3.5px gap */
  .mpa-inv-list tbody td.inv-c-name > .mpa-inv-life-m {
    display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: .3rem; vertical-align: baseline;
    margin-left: -.6rem; max-width: calc(100% + .6rem); }
  .mpa-inv-list tbody td.inv-c-name > .mpa-inv-life-m .mpa-chip { margin-left: 0 !important; }
  /* the other badges' own line (Accounting, Needs updating, Review...):
     their text on the same left edge as the name, too */
  .mpa-inv-list tbody td.inv-c-status { margin-left: -.6rem; }
  /* the chip line keeps only the secondary chips; with none, it takes no space */
  .mpa-inv-list tbody td.inv-c-status:not(:has(.mpa-chip:not(.mpa-inv-life-d .mpa-chip))) { display: none; }
}
/* Tablet Students expanded siblings (revision 3): "SIBLINGS [3 siblings]" on
   one header row, the names underneath. Revision 5: the label + count and the
   names are centred together in the Siblings area; several names share a
   line when they fit, each name wrapping whole. */
.mpa-stu-sib dt { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: .4rem; }
.mpa-stu-sib dd { text-align: center; }
.mpa-stu-sib dt .mpa-chip { text-transform: none; letter-spacing: normal; white-space: nowrap; flex: 0 0 auto; }
.mpa-stu-sib dd { margin-top: .2rem; }
.mpa-stu-sib dd .mpa-link { display: inline-block; max-width: 100%; white-space: normal; }  /* each name wraps whole */
/* Student row menu (students/_row_menu.html; revision 5): sized to its
   longest item plus the normal item padding. */
.dropdown-menu.mpa-stu-menu { min-width: 0; }
/* Students filters on tablets (students/list.html, 768-991px; revision 4):
   two equal columns that use the card's full inner width -- admin:
   Search | Min age, Gender | Max age, Age | Status; staff: Search | Min age,
   Age | Max age -- then Sort across both with Clear in the same row at its
   end, so every row's right edge lines up with Clear's (revision 3 left an
   empty third column the width of Clear down the whole right side).
   Keyboard order is the DOM order. Phones keep the single column. */
@media (min-width: 768px) and (max-width: 991.98px) {
  #student-filters.mpa-filter-bar { --mpa-sf-clear-w: 64px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 1rem; row-gap: .5rem; align-items: center; }
  #student-filters.mpa-sf-admin { grid-template-areas: "q min" "gender max" "age status" "sort sort"; }
  #student-filters.mpa-sf-staff { grid-template-areas: "q min" "age max" "sort sort"; }
  #student-filters > .mpa-filter-field { min-width: 0; flex: none; width: auto; }
  #student-filters .mpa-sf-q { grid-area: q; }
  #student-filters .mpa-sf-gender { grid-area: gender; }
  #student-filters .mpa-sf-age { grid-area: age; }
  #student-filters .mpa-sf-min { grid-area: min; }
  #student-filters .mpa-sf-max { grid-area: max; }
  #student-filters .mpa-sf-status { grid-area: status; }
  #student-filters .mpa-sf-sort { grid-area: sort; margin-right: calc(var(--mpa-sf-clear-w) + 1rem); }
  #student-filters .mpa-sf-clear { grid-area: sort; justify-self: end; width: var(--mpa-sf-clear-w); margin-left: 0 !important;
    align-self: center; justify-content: center; }
}
/* Invoice header (invoices/detail.html; revision 3): from 768px the status /
   metadata block spans the header's full width BELOW the title and actions
   (the actions keep row 1 only) instead of sitting in the narrow column
   beside the toolbar, so the term / Generated / Approved / Published / Due
   line has the room to be one line. */
@media (min-width: 768px) {
  .mpa-invoice-head .mpa-invoice-actions-wrap { grid-row: 1; }
  .mpa-invoice-head .mpa-detail-meta { grid-column: 1 / -1; grid-row: 2; }
}
/* Invoice details lines (invoices/detail.html; revision 4): whole items
   wrap one by one -- no all-or-nothing stacking. Each "Label date" stays
   whole, a separator is glued to the item before it (hidden by the page's
   script when it ends a line), and the gaps between items are the same on
   every row. */
.mpa-meta-line, .mpa-meta-contact { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: .35rem; row-gap: .1rem; }
.mpa-meta-line > .mpa-meta-text { min-width: 0; overflow-wrap: anywhere; }
.mpa-meta-line .mpa-meta-sep, .mpa-meta-contact .mpa-meta-sep { margin: 0 0 0 .35rem; }
.mpa-meta-contact { column-gap: .6rem; }
.mpa-meta-contact .mpa-mc-name { white-space: normal; min-width: 0; }
.mpa-meta-contact .mpa-mc-email { white-space: normal; max-width: 100%; min-width: 0; overflow-wrap: anywhere; }
.mpa-meta-contact .mpa-mc-ref { min-width: 0; }
.mpa-meta-contact.is-wrapped .mpa-mc-ref { flex: 0 0 100%; }
.mpa-meta-contact .mpa-mc-ref-label { white-space: nowrap; }
.mpa-meta-contact .mpa-mc-ref-value { overflow-wrap: anywhere; }
/* Revision 5: a date pair (Generated + Approved; Published + Due) is one
   wrapping unit with the line's own gaps; .is-stacked (the page's script,
   when a pair cannot fit on a line) puts every date on its own row. */
.mpa-meta-line > .mpa-meta-group { display: inline-flex; flex-wrap: wrap; align-items: baseline; column-gap: .35rem; row-gap: .1rem; min-width: 0; }
.mpa-meta-line.is-stacked, .mpa-meta-line.is-stacked > .mpa-meta-group { flex-direction: column; align-items: flex-start; }
/* The "#" glyph sits 4px inside its icon box (the envelope fills its box), so
   it read as indented under the person / envelope icons; nudge the glyph
   only -- the "Payment ref:" text keeps the same left edge as the name and
   email beside their icons. */
.mpa-meta-contact .mpa-mc-ref > .bi-hash { position: relative; left: -2px; }
/* Invoice header status badge (revision 6): it starts its own row, so its
   TEXT lines up with "Invoice details" and the metadata below; the coloured
   background extends into the page gutter by the badge's own padding. */
.mpa-invoice-head .mpa-detail-meta > .mt-1 > .mpa-chip:first-child { margin-left: -.6rem; }
/* Enquiry cards (revision 3): facts wrap whole; the tablet status line reads
   "[Trial arranged] 29 Oct" in one row under the child. */
.mpa-enq-f { white-space: nowrap; }
.mpa-enq-f-name { white-space: normal; }
.mpa-enq-m-email { overflow-wrap: anywhere; }
.mpa-enq-m-status-t { margin: .2rem 0 .1rem; }
.mpa-enq-m-status-t .mpa-enq-status { flex-direction: row; align-items: center; gap: .4rem; }
/* Tablet invoice header (768-991px; revision 3): the actions take their own
   full-width row under the title (they wrapped into three rows beside it,
   leaving a blank band under a short name), and the metadata follows below. */
@media (min-width: 768px) and (max-width: 991.98px) {
  /* revision 6: as wide as the actions need (right-aligned), up to the full
     row -- so a toolbar that fits stays compact, one that does not fills it */
  .mpa-invoice-head .mpa-invoice-actions-wrap { grid-column: 1 / -1; grid-row: 2; justify-self: end; max-width: 100%; }
  .mpa-invoice-head .mpa-detail-meta { grid-row: 3; }
}
/* Published invoice header actions (invoices/detail.html; revision 6): two
   groups in keyboard order -- [Resend email | Void invoice… | Mark as paid]
   and [Download invoice | Correct invoice…]. Each group is one unit: where
   both fit they share one row; otherwise the second group drops to its own
   row whole, and each row's buttons share its full width evenly (never
   below their label width). A missing action simply leaves a shorter group.
   Phones keep the full-width single column. */
@media (min-width: 768px) {
  .mpa-invoice-actions > .mpa-act-group { display: flex; gap: .5rem; flex: 1 1 auto; min-width: 0; }
  .mpa-act-group > * { flex: 1 1 0; min-width: max-content; }
  .mpa-act-group > form > .btn, .mpa-act-group > div > .btn, .mpa-act-group > form > .mpa-btn-tipwrap,
  .mpa-act-group > form > .mpa-btn-tipwrap > .btn { width: 100%; }
  .mpa-act-group > form > .mpa-btn-tipwrap { display: block; }
  .mpa-act-group > a.btn { justify-content: center; }
}
@media (max-width: 767.98px) {
  .mpa-invoice-actions > .mpa-act-group { display: contents; }
  .mpa-act-group > form, .mpa-act-group > div { min-width: 0; }
  .mpa-act-group > form > .btn, .mpa-act-group > div > .btn, .mpa-act-group > a.btn,
  .mpa-act-group > form > .mpa-btn-tipwrap > .btn { width: 100%; }
  .mpa-act-group > form > .mpa-btn-tipwrap { display: block; }
}
/* Invoice header actions on touch screens (revision 6): real 44px targets
   (they were 38px); same buttons, links and forms. */
@media (pointer: coarse) {
  .mpa-invoice-actions .btn { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
}
/* Reopen / Un-mark as paid (paid invoice header; revision 6): an ENABLED
   secondary action that read as disabled -- its border was 1.1:1 against the
   page. Same quieter outline, but a readable label and a visible edge. */
.mpa-invoice-actions a.btn-outline-secondary[href$="/reopen-confirm"] {
  --bs-btn-color: var(--mpa-ink); --bs-btn-border-color: var(--mpa-neutral);
  --bs-btn-hover-border-color: var(--mpa-muted); --bs-btn-active-border-color: var(--mpa-muted); }
/* Phone enquiry card (revision 5): counted from the buttons actually in the
   grid (the inline panels are not buttons). An odd number gives the FIRST
   one -- the status's main action -- the full width and the rest pair up
   beneath it; an even number is all equal half-width cells. */
.mpa-enq-mcard .mpa-enq-actions:has(> :nth-last-child(1 of :not(.collapse, .collapsing)):nth-child(odd of :not(.collapse, .collapsing)))
  > :nth-child(1 of :not(.collapse, .collapsing)) { grid-column: 1 / -1; }
/* Timetable drawer on phones and tablets (revision 4): the sticky identity
   header keeps everything Rory's finding 6 put in it, but its teacher /
   venue chips wrap INSIDE themselves (a long venue + room ran past the
   drawer's right edge) and sit closer to the time line, so less of the
   phone is frozen above the roster. */
@media (max-width: 991.98px) {
  #slot-drawer .mpa-panel-head { padding-top: .45rem; padding-bottom: .4rem !important; }
  #slot-drawer .mpa-panel-chips { margin-top: .3rem !important; }
  #slot-drawer .mpa-panel-chips > .mpa-chip { white-space: normal; max-width: 100%; line-height: 1.2; padding-top: .1rem; padding-bottom: .1rem; }
  /* revision 5: the chip's own gap already separates icon and text */
  #slot-drawer .mpa-panel-chips > .mpa-chip > .bi { margin-right: 0 !important; }
}
/* Drawer identity chips (timetable/panel.html; revision 6), every width:
   one chip per line, and each chip's TEXT starts on the drawer's text column
   (the class name / time line above). The icon and a sliver of padding sit
   in the left gutter (1.25em: 14px on a phone, 11px on desktop, inside the
   16px / 14px gutters); a wrapped venue's second line starts under its first.
   The chip may run to the right content edge. */
#slot-drawer .mpa-panel-chips { flex-direction: column; align-items: flex-start; }
#slot-drawer .mpa-panel-chips > .mpa-chip { gap: 0; padding-left: .15em; padding-right: .5em;
  margin-left: -.15em; max-width: calc(100% + .15em); }
#slot-drawer .mpa-panel-chips > .mpa-chip:has(> .bi) { margin-left: -1.25em; max-width: calc(100% + 1.25em); }
#slot-drawer .mpa-panel-chips > .mpa-chip > .bi { font-size: .9em; width: 1em; margin: 0 .2222em 0 0 !important; flex: 0 0 auto; text-align: center; }
/* ══════════════════════════════════════════════════════════════════════
   TEXT ALIGNMENT RULE -- PROJECT_CONTEXT §9.3.1 rule 17; full text
   docs/UI_TEXT_ALIGNMENT_STANDARD.md. Opt-in component variants (never a
   blanket rule): put the class on the instance that needs it.
   A  A standalone badge / compact action that STARTS a content row lines up
      by its visible TEXT with the content's text column; its own padding
      (and border) sits in the gutter. The gutter must hold that padding
      plus any focus ring (cards and drawers here have >= 13px):
        .mpa-text-start       one .mpa-chip, or one .btn (its own padding)
        .mpa-text-start-row   a row whose FIRST item is a chip
        .mpa-text-start-wrap  a flex-wrap row where badges FOLLOW other
                              content: they line up only once they wrap
        .mpa-badge-sp + .mpa-chip.mpa-badge-after
                              an inline badge after text (inline flow): its
                              gap collapses at a line end, so a badge that
                              wraps starts with its text on the text column
   E  .mpa-icon-text: icon in a fixed gutter, text (and any wrapped line)
      starting on one column -- the text goes in its own element.
   Not for full-width buttons or multi-button rows (rule D).
   ══════════════════════════════════════════════════════════════════════ */
.mpa-chip.mpa-text-start, .mpa-text-start-row, .mpa-text-start-wrap { margin-left: -.6rem; }
.btn.mpa-text-start { margin-left: calc(-1 * (var(--bs-btn-padding-x) + var(--bs-btn-border-width))); }
.mpa-text-start-wrap > :first-child { margin-left: .6rem; }
.mpa-badge-sp { font-size: 0; word-spacing: .85rem; }  /* .6rem pull-back + .25rem gap */
.mpa-chip.mpa-badge-after { margin-left: -.6rem; }
.mpa-icon-text { display: flex; align-items: baseline; column-gap: .35em; }
.mpa-icon-text > .bi:first-child { flex: none; margin: 0 !important; }
.mpa-icon-text > :not(.bi) { min-width: 0; }
/* A rows list INSIDE a padded card body (student "Invoices this term"):
   the body already sets the text column, so the rows drop their own
   1.25rem inset and line up with the section title (r9, Rory). The rows
   are not links, so no hover wash that would touch the text. */
.mpa-rows-flush > .mpa-row { padding-left: 0; padding-right: 0; }
.mpa-rows-flush > .mpa-row:hover { background: transparent; }
/* B  Label / value lists (dl.mpa-dl, dl.mpa-detail-dl -- student, staff,
   class, venue details): a label and its value share ONE first baseline
   (r10, Rory). The label is smaller type than the value, so the label's
   first line carries an empty strut in the value's type; its text then
   sits on the value's first baseline at every width, whatever the two
   sizes are (it was 2px high on every row). A badge that is the value
   keeps its own top padding out of the line box, so the badge's TEXT --
   not its box -- is on that baseline too (it was ~2.9-3.3px low). */
:is(dl.mpa-dl, dl.mpa-detail-dl) > dt::before { content: ""; font-size: var(--mpa-dl-value-size); line-height: var(--mpa-dl-value-lh); }
:is(dl.mpa-dl, dl.mpa-detail-dl) > dd > .mpa-chip { margin-top: calc(-1 * var(--mpa-chip-pad-y)); }
/* Roster names (revision 5): a chip after the name is separated by the
   space before it -- no extra margin on top -- so "Poppy Bennett  from 08 Dec
   Trial" keeps one line whenever it genuinely fits beside Enrol and the
   kebab. The name itself is never shortened. */
.mpa-roster-row > .mpa-roster-name .mpa-chip.ms-1 { margin-left: .1rem !important; }
/* Roster rows with a mouse (revision 4): 2px above and below the 32px
   controls instead of 4px, so a desktop row is 36px, not 40px. Touch
   screens keep their 46px rows (44px targets + separation). */
@media (pointer: fine) {
  .mpa-roster-row.py-1 { padding-top: 2px !important; padding-bottom: 2px !important; }
  .mpa-roster-row.pb-1 { padding-bottom: 2px !important; }
}
/* The roster's Enrol confirmation buttons are real 44px targets on touch
   screens, like every other roster control (revision 4). */
@media (pointer: coarse) {
  .mpa-roster-confirm .btn { min-height: 44px; display: inline-flex; align-items: center; }
}
/* Tablet enquiry card actions on touch screens: 44px targets (revision 4;
   40px stays for a mouse). */
@media (pointer: coarse) {
  .mpa-enq-tactions .btn { min-height: 44px; }
}
