/*
 * Mimir CTMS — the ONE custom stylesheet (FRONTEND.md §6).
 * Loads after beer.min.css.
 *
 * Provenance:
 *   BeerCSS 4.0.23 · htmx 2.0.10 · Inter 4.1 (variable) ·
 *   Material Symbols Rounded @ google/material-design-icons
 *     commit abd7f5c0e179c83f068c770650bd14ebac5d5a09 (see Makefile `assets`).
 *
 *   ── HOW COLOUR WORKS HERE ──────────────────────────────────────────────
 *
 *   §3 THE PALETTE is the ONLY place in this file that writes a colour. It is
 *   the designer's schema, verbatim. To change any colour in the app, edit one
 *   line there and nothing else.
 *
 *   §4 is a BRIDGE: BeerCSS's stylesheet consumes Material 3 role names, so
 *   those names must exist — but every one of them is an alias of a §3 token
 *   and none carries a literal. Nothing is generated; nothing is derived from
 *   a seed colour. If BeerCSS paints something the wrong colour, the fix is a
 *   line in §4, never a new hex.
 *
 *   §5 THE CHART RAMP is the one other block with literals. It is a
 *   CVD-validated categorical ramp with measured ΔE and contrast figures
 *   (FRONTEND.md §4.9), not brand colour, and is not folded into the palette.
 *
 *   NAMING RULE: a colour token has no prefix (--main, --page, --hover). A
 *   --ctms-* token is geometry (heights, widths, gaps, grid columns).
 *
 *   THE ACCENT (§6) is teal and appears in SEVEN places only: the logo's fourth
 *   dot, the fantasy-name wordmark suffix, the navbar's 2px rule, the login
 *   card's 2px rule + auth glows, filled buttons, their hover step, and the
 *   travelling dot of the four-dot loader (§16b — the SEVENTH, added Aug-2026
 *   by owner direction, and the mark's own accent dot in motion rather than a
 *   new use). Everything else that used to be teal — icons, links, focus
 *   rings, tabs, checkboxes, active indicators — is --main. Do not widen this.
 */

/* ---------- 1. Self-hosted fonts (never CDN at runtime, FRONTEND.md §3.3) ---------- */

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/InterVariable.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Inter";
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/InterVariable-Italic.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Material Symbols Rounded";
  font-style: normal;
  font-weight: 100 700;
  font-display: block; /* never show raw ligature text */
  src: url("/static/fonts/material-symbols-rounded.woff2") format("woff2-variations");
}

/* ---------- 3. THE PALETTE — the only colours in this file (FRONTEND.md §3.0.1) ---------- */

/* Declared on :root AND on both body classes on purpose. BeerCSS ships its own
   defaults on `:root, body.light` and `body.dark`; a plain `body` selector (0,0,1)
   LOSES to its `:root` (0,1,0), so in class-less "system" mode the page would show
   BeerCSS purple until its JS stamps a theme class. This list beats it in all
   three states. */
:root,
body.light,
body.dark {
  color-scheme: light dark;

  /* --- Brand --- */
  /* THE main colour: wordmark, logo ink, every active/selected indicator, icons
     of the current item, links, focus rings. NOT the same token as --text: the
     two coincide in light and differ in dark (#f2f5f7 wordmark vs #e6ebf0 body
     text). Do not re-collapse them. */
  --main: light-dark(#263B52, #f2f5f7);
  /* THE accent. Six sanctioned uses — see the header and §6. */
  --accent: light-dark(#1f9e9a, #3fc1bb);
  --accent-hover: light-dark(#188a87, #5acfc9);
  /* Text/icons on an accent fill. In dark the accent is a LIGHT colour, so this
     flips to near-black — never assume white-on-accent. */
  --on-accent: light-dark(#ffffff, #0f1720);

  /* --- Surfaces --- */
  /* THE page: body, navbar, sidebar, the auth page, the chart plotting surface. */
  --page: light-dark(#FDFDFF, #0f1720);
  /* THE card sitting on it. Reserved for genuine card COMPONENTS and floating
     overlays: report cards, dialogs, menus, select popups, the login card. Page
     content — data tables, sections, lists — stays on --page (§4.2). */
  --card: light-dark(#F4F7FA, #1b2a3a);
  /* The two interaction surfaces. --hover is neutral and means "the pointer is
     here"; --selected is the schema's teal-tinted state and means "this is the
     one you are on". Never use one for the other. */
  --hover: light-dark(#ebf1f5, #223447);
  --selected: light-dark(#dcefee, #173a3e);

  /* --- Text --- */
  --text: light-dark(#0F1720, #e6ebf0);
  --text-muted: light-dark(#5b6b7b, #93a1b0);

  /* --- Lines --- */
  --line: light-dark(#ccd6de, #2b3f53);
  --line-strong: light-dark(#8593a1, #5d6f81);
  /* The whisper-thin row rule the borderless tables, lists, timeline and export
     sheet all share (§4.2). Global — components consume it, never redeclare it. */
  --hairline: color-mix(in srgb, var(--line) 55%, transparent);

  /* --- Status (§3.1) ---
     Two values per status: the TEXT colour, and a pre-resolved BACKGROUND tint
     for the chip or banner behind it. The tint is a brighter hue step than the
     text and is laid down at 10% in light and 12% in dark — a percentage cannot
     vary inside one color-mix(), so light-dark() wraps the two finished mixes. */
  --error: light-dark(#b1373a, #d6484b);
  --error-bg: light-dark(color-mix(in srgb, #d6484b 10%, transparent),
                         color-mix(in srgb, #e8686a 12%, transparent));
  /* Light's warning is a fully saturated golden ochre, not the muted brown-amber
     it started as: at 76% saturation the 10% tint washed out to cream and the
     status read "pastel", not "yellow". Dark keeps its own pair unchanged. */
  --warning: light-dark(#8f6600, #d9912b);
  --warning-bg: light-dark(color-mix(in srgb, #f5bd00 10%, transparent),
                           color-mix(in srgb, #f0a83b 12%, transparent));
  --success: light-dark(#1f7a50, #2e9e6b);
  --success-bg: light-dark(color-mix(in srgb, #2e9e6b 10%, transparent),
                           color-mix(in srgb, #4dbe8a 12%, transparent));
  --info: light-dark(#2a66a0, #3b82c4);
  --info-bg: light-dark(color-mix(in srgb, #3b82c4 10%, transparent),
                        color-mix(in srgb, #5a9be0 12%, transparent));
  /* The schema supplies no neutral; it derives from the secondary-text greys so
     "cancelled / inactive / archived" reads as absence of status, not a colour. */
  --neutral: light-dark(#5b6b7b, #93a1b0);
  --neutral-bg: light-dark(color-mix(in srgb, #5b6b7b 10%, transparent),
                           color-mix(in srgb, #93a1b0 12%, transparent));

  /* --- The sidebar's neumorphic lift: the ONE exception (§3.0.5) ---
     On the near-black dark page a black shade is invisible, so dark leans on the
     glow and uses a shade barely darker than the surface. */
  --sidebar-glow: light-dark(rgba(255, 255, 255, 0.9), rgba(120, 150, 175, 0.10));
  --sidebar-shade: light-dark(rgba(70, 90, 115, 0.20), rgba(0, 0, 0, 0.65));
}

/* Theme switching: cookie → server-rendered body class; absent class = follow OS.
   light-dark() resolves per color-scheme, so "system" needs no JS and never flashes. */
body.light {
  color-scheme: light;
}
body.dark {
  color-scheme: dark;
}

/* ---------- 4. BeerCSS / Material 3 bridge — ALIASES ONLY, no literals ---------- */

/* BeerCSS paints with M3 role names. Map them onto the palette; never give one a
   hex of its own. NOTE --primary is the MAIN colour, not the accent: that single
   line is what stops BeerCSS tinting every checkbox, tab, focus ring, field label
   and progress bar teal. Buttons opt into the accent in §6. */
:root,
body.light,
body.dark {
  --primary: var(--main);
  --on-primary: var(--on-accent);
  --primary-container: var(--selected);
  --on-primary-container: var(--main);
  --inverse-primary: var(--main);

  --background: var(--page);
  --surface: var(--page);
  --surface-dim: var(--page);
  /* These two back the .surface-container-lowest / .surface-bright UTILITY
     classes and nothing else in BeerCSS. They point at --page, not --card:
     a utility class is a generic "give this a surface" escape hatch, and
     --card is reserved for card components and floating overlays (§3.1).
     A real card gets .ctms-report-card or its own rule, never a utility. */
  --surface-container-lowest: var(--page);
  --surface-bright: var(--page);
  --surface-container-low: var(--hover);
  --surface-container: var(--hover);
  --surface-container-high: var(--hover);
  --surface-container-highest: var(--hover);
  --surface-variant: var(--hover);

  --on-surface: var(--text);
  --on-background: var(--text);
  --inverse-surface: var(--text);
  --inverse-on-surface: var(--card);
  --on-surface-variant: var(--text-muted);

  --outline: var(--line-strong);
  --outline-variant: var(--line);

  --error-container: var(--error-bg);
  --on-error-container: var(--error);
  --on-error: var(--on-accent);

  /* The M3 long tail the schema never names. Previously eight generated hexes
     from a seed colour, which leaked a second and third hue into filled chips
     and the calendar "today" marker; now plain palette aliases. */
  --secondary: var(--text-muted);
  --on-secondary: var(--card);
  --secondary-container: var(--selected);
  --on-secondary-container: var(--main);
  --tertiary: var(--main);
  --on-tertiary: var(--on-accent);
  --tertiary-container: var(--selected);
  --on-tertiary-container: var(--main);

  --shadow: #000000;
  --scrim: #000000;
}


:root,
body.light,
body.dark {
  /* One colour for every bar of one series. Length is the magnitude; colouring
     by magnitude too would restate it. */
  --chart-series: light-dark(#1f9e9a, #02a99b);
  /* De-emphasis: shown for completeness, not a step anybody takes. */
  --chart-muted: light-dark(#5b6b7b, #93a1b0);
  /* The outcome the report is about, and it is GREEN — tied to --success so an
     accented figure and a success chip cannot drift apart. */
  --chart-terminal: light-dark(#1f7a50, #2e9e6b);
  /* The unfilled track behind a bar, so a short bar still reads as a share of
     something rather than as a stub. */
  --chart-track: color-mix(in srgb, var(--text) 7%, transparent);
  /* THE CATEGORICAL RAMP — this app's colour that means IDENTITY, not magnitude. */
  --chart-cat-1: light-dark(#1f9e9a, #02a99b);
  --chart-cat-2: light-dark(#834100, #9e520c);
  --chart-cat-3: light-dark(#4a64db, #3d53e5);
  --chart-cat-4: light-dark(#8b0e87, #993f94);
  --chart-cat-5: light-dark(#ee4d68, #f43960);
}

/* ---------- 6. THE ACCENT SCOPE (FRONTEND.md §3.0.4) ---------- */

/* Teal exists HERE and in four brand marks (.brand-accent-text below, the navbar
   rule in §9, the login-card rule and auth glows in §13). Nowhere else.

   A filled button opts in by redefining --primary locally rather than by being
   repainted, so BeerCSS's own disabled / .border / .transparent / .chip handling
   keeps working untouched. Outlined buttons deliberately stay --main: filled teal
   is the action, outlined ink is the alternative. */
:is(button, .button):not(.transparent):not(.border):not(.chip) {
  --primary: var(--accent);
  --on-primary: var(--on-accent);
}

/* A FILLED button's hover is the schema's accent-hover step rather than BeerCSS's
   generic translucent overlay — the brand names an actual second colour, so state
   it. Only filled buttons: transparent/border/error variants keep their own. */
button:not(.transparent):not(.border):not(.error):not(.chip):not(:disabled):is(:hover, :focus-visible) {
  background: var(--accent-hover);
}

/* The wordmark suffix — fantasy name in the navbar, "CTMS" in the product lockup,
   module label in the sidebar (§3.0.2). Solid accent, no gradient. */
.brand-accent-text {
  color: var(--accent);
}

/* ---------- 7. Status chips — always label text, never colour alone (§3.1) ---------- */

.chip.status-success { --_status: var(--success);  --_status-bg: var(--success-bg); }
.chip.status-warning { --_status: var(--warning);  --_status-bg: var(--warning-bg); }
.chip.status-error   { --_status: var(--error);    --_status-bg: var(--error-bg); }
.chip.status-info    { --_status: var(--info);     --_status-bg: var(--info-bg); }
.chip.status-neutral { --_status: var(--neutral);  --_status-bg: var(--neutral-bg); }
.chip[class*="status-"] {
  border: none; /* flat design: tint + label, no chrome */
  background: var(--_status-bg);
  color: var(--_status);
  font-weight: 400;
  border-radius: 2rem; /* full pill */
}

/* ---------- 8. Layout variables (geometry only — see the NAMING RULE) ---------- */

body {
  --font-icon: "Material Symbols Rounded";
  /* What a sticky header/footer paints itself with. It must match the surface it
     is pinned INSIDE, or the rows scrolling under it show through the gap; a
     filled card overrides this locally rather than every sticky rule guessing. */
  --sticky-bg: var(--page);
  --ctms-navbar-height: 64px;
  /* This bar is the ONLY air between the navbar and the page's opening block:
     #main-content is the scrollport, so top padding there would push the sticky
     constraint rect down and float a sticky table head below the fold's top
     edge (the trap §14's padding-bottom note records). The crumb is centred, so
     the height buys the gap above it AND the gap under it in one step — at 44px
     the crumb sat 13px under the navbar and the page title 21px under the crumb,
     both too tight (owner direction, Aug-2026). */
  --ctms-breadcrumbs-height: 60px;
  --ctms-gap: 3.5rem;
  /* The rail is always 80px for layout purposes; hover-expansion (when unlocked)
     widens only the card itself, overlaying the content (§2.2). */
  --ctms-sidebar-width: 80px;
  --ctms-sidebar-expanded-width: 256px;
  --ctms-content-offset: calc(var(--ctms-sidebar-width) + 2 * var(--ctms-gap));
  /* The sign-off bar (§2.6). Like the crumb bar this is a declared height the
     shell budgets out of #main-content, so it must stay a number the scroll
     model can subtract — never `auto`. The height is set by the mono lockup it
     seats (24px + 12px of air each side), and the lockup's floor is legibility:
     the mono asset bakes `CTMS` in as artwork at the reference's own 26/130
     proportion, so unlike the navbar lockup's live-text suffix it cannot be
     opened up independently (§3.0.2). Below ~20px the acronym stops being read
     and starts being a smudge; 24px is the first size that holds it. */
  --ctms-footer-height: 48px;
}

/* ---------- 9. Base ---------- */

body {
  background: var(--page);
  color: var(--text);
  font-size: 0.875rem; /* 14px body (§3.3) */
}

/* The title a module section opens with — "Recruitment pipeline", "Recruitment
   statuses", "Medical records". Brand ink, not body text: it names the PLACE you
   are in, which is the same job the current breadcrumb and the active sidebar
   label already do in --main (§3.0.4), so all three now say it in one colour. In
   dark it lifts clear of the body text beneath it — the exact separation --main
   exists for (§3.0.1); do not collapse it back to --text. */
.page-title {
  color: var(--main);
  font-size: 1.375rem;
  font-weight: 600;
  line-height: 1.3;
  margin: 0;
}
.section-heading {
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.4;
  margin: 0;
}
.text-secondary {
  color: var(--text-muted);
  font-size: 0.75rem;
}
.tabular-nums {
  font-variant-numeric: tabular-nums;
}
/* Copy-friendly identifiers: subject IDs, sample IDs, kit numbers (§4.2).
   Never wrapped: an identifier is read aloud and typed back, so breaking it
   across two lines makes it misread (M4.1). */
.mono-id {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  font-weight: 500;
  user-select: all;
  white-space: nowrap;
}

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

h1[tabindex="-1"],
h2[tabindex="-1"] {
  outline: none; /* focus target after HTMX swap (§7); no ring on programmatic focus */
}

/* ---------- 10. App shell scroll model ---------- */
/* #main-content is the single scrollable region (§2.4); navbar, sidebar and
   breadcrumbs never scroll. */

body.ctms-shell {
  display: block; /* opt out of BeerCSS's body grid — the shell owns its layout */
  height: 100dvh;
  overflow: hidden;
  padding-top: var(--ctms-navbar-height);
}

/* ---------- 11. Navbar (§2.1) ---------- */

.ctms-navbar {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--ctms-navbar-height);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 var(--ctms-gap);
  /* Flat: same surface as the page; the accent hairline below is the only
     separation (§2.1). */
  background: var(--page);
  z-index: 20;
}
.ctms-navbar::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  background: var(--accent);
}
/* The accent hairline is the navbar's LAST positioned child, so at z-index auto
   it paints over any open dropdown that hangs below the bar. Lift the navbar's
   menu wrappers above it (the hairline stays visible either side of the menu). */
.ctms-navbar .ctms-menu-wrap {
  z-index: 1;
}
.ctms-navbar .max {
  flex: 1;
}
.ctms-navbar .ctms-user-name {
  font-weight: 500;
  white-space: nowrap;
}
/* The logout icon button is a one-button POST form (CSRF); the form wrapper
   must not disturb the navbar's flex row. Mobile hiding still comes from the
   more specific .ctms-navbar .ctms-navbar-wide rule. BeerCSS's global sibling
   spacing rule (beer.min.css) adds margin-block-start to any <form> following
   a non-.grid/nav/.row sibling; its :not()/:is() compound selector outweighs
   two plain classes, so plain-cascade overrides lose — !important is required
   (same as the .tooltip override below, §13). */
.ctms-navbar .ctms-logout-form {
  display: inline-flex;
  align-items: center;
  margin-block-start: 0 !important;
}

/* Brand lockup (§3.0.2): the delivered logo — four-dot mark plus the "Mimir"
   wordmark, inlined as two SVGs cut from the one asset — then the accent
   suffix. Both SVGs are cropped to the artwork, so --ctms-brand-height IS the
   wordmark's cap height and the mark's height at once: the designer's
   equal-height lockup holds from one number, and every gap and type size below
   is a ratio of it, measured off the delivered `Mimir CTMS` lockup (its
   artwork is 130 units tall, so a ratio here is "n of 130"). Baseline
   alignment throughout — each SVG's bottom edge IS the wordmark baseline, so
   the suffix sits on the same line as the M (the reference puts CTMS's
   baseline at 186 against the wordmark's 185) rather than box-centred. */
.ctms-brand-lockup {
  --ctms-brand-height: 1.75rem;
  --ctms-brand-suffix-ratio: 0.457;
  display: inline-flex;
  align-items: baseline;
  /* The asset's own mark-to-wordmark air: 57 of its 130 height units. */
  gap: calc(var(--ctms-brand-height) * 0.44);
  text-decoration: none;
  color: inherit;
}
.ctms-brand-lockup svg {
  display: block;
  height: var(--ctms-brand-height);
  width: auto;
}
/* BeerCSS's `*{border-radius:inherit}` bleeds ancestor radii into the inline
   logo and deforms it; reset so the official geometry renders. */
.ctms-brand-lockup svg,
.ctms-brand-lockup svg path {
  border-radius: 0;
}
.ctms-brand-lockup .ctms-wordmark {
  display: inline-flex;
  align-items: baseline;
  /* Wordmark-to-suffix air: 19 of the asset's 130 height units. */
  gap: calc(var(--ctms-brand-height) * 0.146);
  white-space: nowrap;
}
/* The module fantasy name / CTMS suffix, kept as a RATIO of the logo height so
   the whole lockup still rescales from one line — sized off the delivered
   `Mimir CTMS` reference, then opened up one step:
     - Size: the reference's suffix is 26 units of cap height on the 130-unit
       logo. Inter's cap is 0.667em, so the cap-matched face is 26/0.667 = 39
       units = 0.30 of logo height. We run well above that 0.30 because the
       navbar logo is 28px, not the reference's display size: at the reference
       ratio the module name lands near 8px and reads as fine print beside the
       mark, so it was opened up until it holds its own — 12.8px, i.e. 0.457
       of the navbar's 28px lockup. The ratio is a variable, not a constant,
       precisely because it is height-dependent: the navbar was later trimmed
       from 32px to 28px to sit better in the bar and the ratio went 0.40 →
       0.457 in the same breath so the fantasy name did NOT shrink with it.
       Any context that sets its own --ctms-brand-height (the login card,
       §11) sets its own ratio too — under ~10px the tracked mixed-case name
       stops being read.
     - Tracking: the reference's inter-letter ink gaps are 7/8/8 units; Inter
       Bold's own, at that size, are 3.5/1.2/3.1. The 5.07-unit average
       shortfall on a 39-unit face is 0.13em. (Inter is the wider face, so the
       word ends up wider than the reference's — the tracking is what carries
       across, per §3.3: Inter is the UI face, the lockup art is not.)
   Casing stays the registry's (eTrials, myAgenda), not the reference's caps:
   camelCase is part of those names (§3.0.3). Tracking adds a trailing space
   after the last letter that the lockup does not own — pull it back so the
   gap to the module dropdown stays the one the navbar set. */
.ctms-brand-lockup .brand-accent-text {
  font-size: calc(var(--ctms-brand-height) * var(--ctms-brand-suffix-ratio));
  font-weight: 700;
  letter-spacing: 0.13em;
  margin-inline-end: -0.13em;
  line-height: 1;
}

/* Dropdown menus (BeerCSS <menu>) — restyled to the flat design language:
   card surface above the page, soft airy shadow, roomy rounded items, and a
   primary wash on hover instead of BeerCSS's gray fill. */
menu {
  background: var(--card);
  border-radius: 14px;
  box-shadow: 0 8px 28px light-dark(rgba(45, 55, 90, 0.18), rgba(0, 0, 0, 0.6));
  padding: 0.5rem;
}
menu > li {
  border-radius: 8px;
  gap: 0.875rem;
}
menu > li:hover {
  background: var(--hover);
}
/* THE one "selected option" treatment, shared by every dropdown in the app
   (owner direction): the row gets a rounded primary tint, slightly stronger
   than hover, alongside each menu's own non-color cue (filled icon, check).
   Any menu item carrying aria-current — the active module, the bound site —
   picks it up automatically. Native <select> pickers mirror it (§13). */
menu > li:has([aria-current]) {
  background: var(--selected);
}
/* The li wash above is the ONLY hover effect: kill the ripple/state layer of
   buttons inside menu items (e.g. the theme-switch forms) — otherwise both
   paint and the item shows a second, inner hover pill. */
menu button::after {
  display: none;
}
/* Breathing room between a navbar trigger and its open menu: BeerCSS pins the
   menu to the wrapper's bottom edge and translates it down by its own height,
   so pushing that edge below the wrapper is what opens the gap. Scoped to the
   navbar — the sidebar account popup opens UPWARD and sets its own inset. */
.ctms-navbar .ctms-menu-wrap > menu {
  inset-block-end: -0.625rem;
}

/* Menus open on CLICK (focus), never on mere hover — BeerCSS's
   `:hover > menu` auto-open also kept a closed menu visible while the pointer
   was still on the trigger after a second (closing) click. */
.ctms-menu-wrap:not(:focus-within):hover > menu {
  opacity: 0;
  visibility: hidden;
}

/* Dropdown wrapper: trigger button + sibling <menu>; BeerCSS opens the menu
   on :focus-within of this wrapper. */
.ctms-menu-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}
.ctms-menu-wrap > menu li.ctms-user-name {
  color: var(--text-muted);
}

/* Theme toggle: both switch items render; exactly ONE shows, matching the
   EFFECTIVE theme — explicit body class first, else the OS scheme. This keeps
   "system" as the cookie-less default with a single menu entry and no JS. */
body.dark .ctms-theme-set-dark,
body.light .ctms-theme-set-light {
  display: none;
}
@media (prefers-color-scheme: dark) {
  body:not(.light) .ctms-theme-set-dark {
    display: none;
  }
}
@media (prefers-color-scheme: light) {
  body:not(.dark) .ctms-theme-set-light {
    display: none;
  }
}

/* Language submenu: a native <details> inside a menu item — expandable with
   no JS, and the dropdown stays open because focus stays inside the wrap.
   The <li> gives up its own layout/hover to the summary and nested items. */
menu > li.ctms-menu-sub {
  display: block;
  padding: 0;
}
menu > li.ctms-menu-sub:hover {
  background: transparent;
}
.ctms-menu-sub details {
  width: 100%;
}
.ctms-menu-sub summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.5rem 0.75rem;
  border-radius: 8px;
  cursor: pointer;
}
.ctms-menu-sub summary::-webkit-details-marker {
  display: none;
}
.ctms-menu-sub summary:hover,
.ctms-menu-sub ul li:hover {
  background: var(--hover);
}
.ctms-menu-sub .ctms-submenu-caret {
  margin-inline-start: auto;
  transition: transform 0.15s ease;
}
.ctms-menu-sub details[open] .ctms-submenu-caret {
  transform: rotate(180deg);
}
.ctms-menu-sub ul {
  list-style: none;
  margin: 0;
  /* Indent entries under the summary label (icon width + gap). */
  padding: 0 0 0 2rem;
}
.ctms-menu-sub ul li {
  border-radius: 8px;
}
/* BeerCSS buttons center their content; submenu entries left-align to read
   as list items under the summary label. */
.ctms-menu-sub ul button {
  justify-content: flex-start;
}

/* Module selector (§2.1). Trigger: the descriptive module name alone — the
   fantasy name lives in the brand lockup beside it — caret flips while the
   menu is open. */
/* On wide screens the selector centers on the navbar rather than trailing the
   lockup: it is taken out of the flex row and pinned to the bar's midpoint, so
   it stays optically centered whatever the right-side controls weigh. Below
   1024px there isn't room between the lockup and those controls, so it falls
   back to its in-flow position next to the lockup. */
@media (min-width: 1024px) {
  .ctms-navbar .ctms-module-select {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
}
.ctms-module-trigger .ctms-module-trigger-name {
  font-weight: 600;
}
.ctms-menu-wrap > button .ctms-dropdown-caret {
  transition: transform 0.15s ease;
}
.ctms-menu-wrap:focus-within > button .ctms-dropdown-caret {
  transform: rotate(180deg);
}

/* Module menu: the shared rounded-tint option treatment (above) — primary
   icon plus the module name, filled icon as the active module's non-color
   cue. */
.ctms-module-menu > li {
  padding: 0;
}
.ctms-module-menu > li > a {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  width: 100%;
  min-height: 48px;
  padding: 0.375rem 1rem 0.375rem 0.75rem;
  border-radius: 8px;
}
.ctms-module-menu > li > a > i {
  color: var(--main);
  flex: none;
}
.ctms-module-menu > li > a[aria-current="page"] > i {
  font-variation-settings: "FILL" 1; /* filled glyph is the current-item cue */
}
.ctms-module-option-name {
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.3;
}

/* Mobile-only navbar controls (§8). */
.ctms-menu-button {
  display: none;
}

/* ---------- 12. Breadcrumbs (§2.3) ---------- */

#breadcrumbs {
  height: var(--ctms-breadcrumbs-height);
  /* The shell divides the viewport between the navbar, this bar and #main-content
     by their declared heights, so this bar may own no margin at all: BeerCSS's
     block-sibling rule (`… + :is(…, nav, …)`) hands it 1rem for following the
     navbar, and that unbudgeted 16px pushed #main-content's bottom edge —
     including the bottom of a sticky form footer — below the fold. */
  margin-block: 0;
  margin-inline-start: var(--ctms-content-offset);
  display: flex;
  align-items: center;
  padding-left: 0.5rem;
  padding-right: var(--ctms-gap);
  font-size: 0.8125rem;
  color: var(--text-muted);
}
#breadcrumbs ol {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
#breadcrumbs li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
#breadcrumbs li + li::before {
  content: "/";
  color: var(--line-strong);
}
#breadcrumbs a {
  color: var(--text-muted);
  text-decoration: none;
}
#breadcrumbs a:hover {
  color: var(--main);
  text-decoration: underline;
}
#breadcrumbs [aria-current="page"] {
  color: var(--main); /* "you are here" — main is the emphasis colour (§3.0.4) */
  font-weight: 500;
}

/* ---------- 13. Sidebar — the ONE neumorphic component (§2.2, §3.0.5) ---------- */

#sidebar {
  position: fixed;
  left: 2rem;
  /* 80vh card, floating vertically CENTERED between the navbar and the
     viewport bottom: fixed height + both insets + auto block margins. The
     section group centers itself in the leftover interior space. */
  top: var(--ctms-navbar-height);
  /* "Between the navbar and the viewport bottom" (§2.2) became "between the
     navbar and the footer" when the footer arrived: the card is centred in the
     slack between its insets, so a footer left out of the bottom inset is a
     footer the card drifts into. The old --ctms-gap alone still cleared it on a
     tall window and collided below ~520px of viewport — near enough to be luck
     rather than layout. */
  bottom: calc(var(--ctms-gap) + var(--ctms-footer-height));
  height: 80vh;
  max-height: calc(100dvh - var(--ctms-navbar-height) - var(--ctms-gap) - var(--ctms-footer-height));
  margin: auto 0;
  width: var(--ctms-sidebar-width);
  display: flex;
  flex-direction: column;
  /* BeerCSS nav centers children; stretch so item washes span the full card
     width and the active bar sits flush with the card's left edge. */
  align-items: stretch;
  gap: 0;
  /* Pure neumorphism: same background as the page — only the dual soft shadow
     lifts the card (§2.2). */
  background: var(--page);
  border-radius: 20px;
  box-shadow:
    -6px -6px 14px var(--sidebar-glow),
    8px 8px 20px var(--sidebar-shade);
  padding: 2rem 0 2rem;
  /* Above BeerCSS field icons (z-index 10): when the card overlays content —
     hover expansion, mobile drawer — select carets must not poke through. */
  z-index: 15;
  transition: width 0.2s ease;
}
#sidebar .ctms-sidebar-items {
  flex: 1;
  overflow-y: auto;
  /* Item tooltips reach outside the card; without this they register as
     horizontal scrollable overflow and a horizontal scrollbar appears. */
  overflow-x: hidden;
  scrollbar-width: none; /* hidden until hover */
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 0.5rem 0;
}
/* Center the section group vertically in the rail. Auto margins instead of
   justify-content:center so a scrolling list keeps its top reachable. */
#sidebar .ctms-sidebar-items > :first-child {
  margin-block-start: auto;
}
#sidebar .ctms-sidebar-items > :last-child {
  margin-block-end: auto;
}
#sidebar .ctms-sidebar-items:hover {
  scrollbar-width: thin;
}

/* Lock toggle at the bottom: locked (default) keeps the rail collapsed;
   unlocked lets it expand on hover. */
.ctms-sidebar-lock {
  align-self: center;
  margin-top: 0.75rem;
}

/* Account popup: on mobile the navbar's right-side controls (§8) collapse into
   ONE settings button that lives in the drawer footer and opens the options as
   a click-to-open popup (reusing the .ctms-menu-wrap focus-within behavior).
   Hidden on desktop — the navbar carries those controls there. */
.ctms-sidebar-account {
  display: none;
}
/* The popup opens UPWARD from the footer button (it sits at the drawer bottom)
   and is centered over it, spanning most of the drawer width so the option
   labels fit; BeerCSS still owns show/hide (opacity/visibility on focus). */
.ctms-sidebar-account > menu {
  inset: auto auto calc(100% + 0.5rem) 50%;
  width: 216px;
  transform: translateX(-50%) scale(0.9);
  transform-origin: bottom center;
}
.ctms-sidebar-account:focus-within > menu {
  transform: translateX(-50%) scale(1);
}

/* Module emblem at the top of the rail: the active module's icon; the accent
   fantasy name appears beside it on hover-expansion (sanctioned use, §3.0.4). */
.ctms-sidebar-module {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.875rem;
  min-height: 56px;
  white-space: nowrap;
}
.ctms-sidebar-module > i {
  color: var(--main);
  font-size: 1.75rem;
}
.ctms-sidebar-module .ctms-sidebar-label {
  display: none;
  font-size: 1rem;
  font-weight: 700;
}

/* Items: full-width hit areas, rectangular washes bleeding from the left edge.
   (Radius reset needed — BeerCSS's `*{border-radius:inherit}` would round the
   washes into pills via the card's 20px radius.) */
#sidebar .ctms-sidebar-items,
.ctms-sidebar-item {
  border-radius: 0;
}
.ctms-sidebar-item {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center; /* collapsed rail: centered icon, no label */
  gap: 0.875rem;
  min-height: 56px;
  padding: 0;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}
.ctms-sidebar-item > i {
  /* THE MAIN COLOUR, at rest and when current (§3.0.4). The active item is told
     apart by its filled glyph, its 3px bar and the --selected wash — not by
     being the only item whose icon carries a colour. */
  color: var(--main);
  flex: none;
}
.ctms-sidebar-item .ctms-sidebar-label {
  display: none;
  font-size: 0.875rem;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Flat fills, no gradients: the wash is one solid colour edge to edge (owner
   direction, Aug-2026 — the left-to-right fade read as a gradient, which the
   brand does not have anywhere, §3.0.4). */
.ctms-sidebar-item:hover {
  background: var(--hover);
}
/* Active = the --selected surface (flat, no fade) with the 3px accent bar on the
   left edge. The accent is the BAR only — the eighth sanctioned accent use
   (§3.0.4); a full accent fill, like the solid --main inversion before it, read
   too harsh at rail size (owner direction, Aug-2026). */
.ctms-sidebar-item[aria-current="page"] {
  background: var(--selected);
}
.ctms-sidebar-item[aria-current="page"]::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--accent);
}
.ctms-sidebar-item[aria-current="page"] > i,
.ctms-sidebar-item[aria-current="page"] .ctms-sidebar-label {
  color: var(--main);
}
.ctms-sidebar-item[aria-current="page"] > i {
  font-variation-settings: "FILL" 1; /* filled icon variant when active (§3.4) */
}

/* Hover-expansion, only when unlocked: the card widens over the content and
   labels appear; the layout offset (--ctms-sidebar-width) never changes. */
body.sidebar-unlocked #sidebar:hover {
  width: var(--ctms-sidebar-expanded-width);
}
body.sidebar-unlocked #sidebar:hover :is(.ctms-sidebar-item, .ctms-sidebar-module) {
  justify-content: flex-start;
  padding: 0 1rem 0 1.75rem;
}
body.sidebar-unlocked #sidebar:hover .ctms-sidebar-label {
  display: block;
}
/* Tooltips only make sense while the rail stays collapsed (locked); unlocked
   hover shows the labels themselves. The mobile drawer shows labels too. */
body.sidebar-unlocked .ctms-sidebar-item > .tooltip,
body.sidebar-open .ctms-sidebar-item .tooltip {
  display: none;
}

/* Item tooltips escape the items list's overflow clip via fixed positioning;
   coordinates are set per-hover in app.js (CSS cannot anchor a fixed tooltip
   to its item). Show/hide still comes from BeerCSS's :hover rules. */
#sidebar .ctms-sidebar-item > .tooltip {
  position: fixed;
  inset: auto;
  margin: 0 !important;
  transform: translateY(-50%) scale(0.9);
}
#sidebar .ctms-sidebar-item:is(:hover, :focus) > .tooltip {
  transform: translateY(-50%) scale(1);
}

/* ---------- 14. Main content (§2.4) ---------- */

#main-content {
  margin-inline-start: var(--ctms-content-offset);
  height: calc(100dvh - var(--ctms-navbar-height) - var(--ctms-breadcrumbs-height) - var(--ctms-footer-height));
  overflow-y: auto;
  padding-right: var(--ctms-gap);
  /* BeerCSS pads a bare <main> on all four sides (`--_padding: 0.5rem`), and
     overriding only padding-right left that 8px on the bottom. This is THE
     scrollport, so a sticky child's `bottom: 0` rests on the padding rather than
     on the container's edge — leaving an 8px strip under a sticky form footer
     through which whatever is being scrolled past keeps showing. */
  padding-bottom: 0;
}
.content-narrow {
  max-width: 880px; /* forms & reading-heavy views (§2.4) */
}

/* ---------- 14b. App footer (§2.6) ---------- */

/* The shell's sign-off bar. It is a SIBLING of #main-content, never a child:
   #main-content is swapped innerHTML on section navigation (§5.1), so a footer
   inside it would be wiped by the first sidebar click. The shell divides the
   viewport by declared heights, so — exactly like #breadcrumbs — this bar owns
   no margin and its height is budgeted out of the scrollport above. */
.ctms-footer {
  /* BeerCSS styles a bare <footer> as a centred grid with 1rem inline padding
     and a 5rem floor (`footer,header{display:grid;…}` + `footer{min-block-size:
     5rem}`). All three would blow the bar's declared height, so all three are
     undone here rather than worked around. */
  display: flex;
  align-items: center;
  min-block-size: 0;
  height: var(--ctms-footer-height);
  margin-block: 0;
  margin-inline-start: var(--ctms-content-offset);
  /* Start where the crumb trail and the page content start: the content offset
     plus the same 8px both of them carry. */
  padding: 0 var(--ctms-gap) 0 0.5rem;
  /* The ONLY thing separating the strapline from the build, so it has to beat
     the word-spacing around that strapline's own "·" separators — at 0.75rem
     the boundary between the two items was invisible. */
  gap: 1.5rem;
  /* One line, always: the shell budgets this bar as a fixed height, so there is
     no second line for anything to wrap onto. */
  white-space: nowrap;
  /* Years and build hashes read better without the proportional-width wobble. */
  font-variant-numeric: tabular-nums;
  /* Flat and borderless (§3): the page fill runs on under the bar and a single
     hairline is its only separation from the content scrolling past it. */
  background: var(--page);
  border-block-start: 0.0625rem solid var(--hairline);
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* The mono lockup (§3.0.2). It sat in this bar until the owner took it out
   (Aug-2026) and currently has NO consumer — the rules stay because the cut is
   a brand asset, not footer chrome, and the component is still documented.
   One ink, sized by height like every other cut of the asset, cropped viewBox
   carrying its own aspect ratio. It has no coupled accent suffix, so the ratio
   table the navbar lockup runs on does not apply — a single object with a
   single measurement. A consumer must set its own height; 1.5rem is the size
   the footer needed and is kept as the documented floor for the acronym. */
.ctms-brand-mono {
  display: block;
  height: 1.5rem;
  width: auto;
  flex: none;
}
/* THE MONO LOCKUP IS ONE INK, AND THE INK IS --main (§3.0.1) — the logo colour,
   not the muted text colour the rest of the bar inherits. One rule rather than a
   fill on each of the fifteen paths: this is the whole point of a mono cut, and
   it beats the SVG's own fill="none" because a CSS rule outranks a presentation
   attribute. Change the lockup's colour here. */
.ctms-brand-mono path {
  fill: var(--main);
}
/* BeerCSS's `*{border-radius:inherit}` bleeds ancestor radii into the inline
   logo and deforms it (same reset as .ctms-brand-lockup). */
.ctms-brand-mono,
.ctms-brand-mono path {
  border-radius: 0;
}

/* The narrow cut is the exception, so the wide one is the default (§8). */
.ctms-footer-copy-short {
  display: none;
}
/* TWO GROUPS, ONE AUTO MARGIN (owner direction, Aug-2026): the copyright reads
   from the left edge, the strapline and build sit together against the right.
   The margin rides the FIRST element of the right-hand group and pushes
   everything after it over, so the pair stays adjacent. On a phone the
   strapline is gone, and the mobile block hands the margin to the build — a
   display:none element is out of layout, so its auto margin would do nothing
   and the split would collapse. */
.ctms-footer-compliance {
  margin-inline-start: auto;
}

/* ---------- 15. Auth layout & login card (§2.5) ---------- */

/* Owner-sanctioned hero accent (§3.0.4 allows it on the login screen only):
   two soft brand-colored glows behind the card, echoing the gradient's 135°
   diagonal (start color top-left, end color bottom-right, like the logo dot).
   Built from the brand tokens via color-mix; dark mode mixes stronger so the
   glow stays visible on the darker surface. */
body.ctms-auth {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--ctms-gap);
  /* The login hero accent (§3.0.4): two soft washes of the brand accent. The
     second is weaker so the pair still reads as one light source. */
  --auth-glow-a: light-dark(
    color-mix(in srgb, var(--accent) 34%, transparent),
    color-mix(in srgb, var(--accent) 26%, transparent));
  --auth-glow-b: light-dark(
    color-mix(in srgb, var(--accent) 20%, transparent),
    color-mix(in srgb, var(--accent) 16%, transparent));
  background: var(--page);
}
/* The glows live on fixed pseudo-elements rather than the body background so
   they can drift into place on load — transform/opacity only (§3.5). The
   longer-than-usual, staggered durations are part of the sanctioned login
   hero moment (§3.0.4), not a precedent for app-shell motion. Negative
   z-index paints them above the page background but under the card. */
body.ctms-auth::before,
body.ctms-auth::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
body.ctms-auth::before {
  background: radial-gradient(48rem 32rem at 14% -6%, var(--auth-glow-a), transparent 70%);
  animation: ctms-glow-in-a 1100ms cubic-bezier(0.2, 0, 0, 1);
}
body.ctms-auth::after {
  background: radial-gradient(44rem 30rem at 104% 106%, var(--auth-glow-b), transparent 70%);
  animation: ctms-glow-in-b 1300ms cubic-bezier(0.2, 0, 0, 1);
}
/* from-only keyframes: the element's static styles ARE the final state, so
   §15's animation:none leaves reduced-motion users with the finished scene.
   Each glow converges toward the center along the brand diagonal. */
@keyframes ctms-glow-in-a {
  from {
    opacity: 0;
    transform: translate(-5rem, -4rem);
  }
}
@keyframes ctms-glow-in-b {
  from {
    opacity: 0;
    transform: translate(5rem, 4rem);
  }
}
/* Borderless card (flat direction): separation comes from the soft shadow —
   the login card is a sanctioned elevated surface (§3.0.5) — plus a 2px
   accent top hairline mirroring the navbar's bottom one. */
.ctms-login-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: var(--card);
  border-radius: 16px;
  box-shadow: 0 12px 32px light-dark(rgba(45, 55, 90, 0.14), rgba(0, 0, 0, 0.5));
  padding: 32px 28px;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  animation: ctms-card-in 200ms ease-out; /* §15 kills this under reduced motion */
}
/* Card rhythm: the flex gaps govern. BeerCSS's blanket sibling rule
   (margin-block-start on p/h1/ul/…) hits some children and skips others
   (buttons, forms), which reads as glued-or-gaping spacing — neutralize it
   here so every space inside the card is deliberate. */
.ctms-login-card > *,
.ctms-login-card form > *,
.ctms-login-card .ctms-print-area > *,
.ctms-mfa-steps > li > * {
  margin-block-start: 0;
}
.ctms-login-card form {
  display: flex;
  flex-direction: column;
  gap: 1.125rem;
}
/* A touch more air before the primary action than between fields. */
.ctms-login-card form > button[type="submit"] {
  margin-block-start: 0.25rem;
}
.ctms-login-card::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  /* pseudo-elements don't get BeerCSS's inherited radius — restate the top */
  border-radius: 16px 16px 0 0;
  background: var(--accent);
}
@keyframes ctms-card-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}
.ctms-login-card .ctms-brand-lockup {
  align-self: center;
  /* The product lockup is the auth card's hero — a step up from the navbar's.
     At 36px the reference-derived ratio needs less opening up than the
     navbar's does: 0.4 puts `CTMS` at 14.4px. */
  --ctms-brand-height: 2.25rem;
  --ctms-brand-suffix-ratio: 0.4;
}
.ctms-google-button img,
.ctms-google-button svg {
  width: 1.125rem;
  height: 1.125rem;
}
.ctms-password-toggle {
  position: absolute;
  right: 0.25rem;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
}
.ctms-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--text-muted);
  font-size: 0.75rem;
}
.ctms-divider::before,
.ctms-divider::after {
  content: "";
  flex: 1;
  height: 0.0625rem;
  background: var(--line);
}
.ctms-login-links {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.375rem;
  text-align: center;
  font-size: 0.8125rem;
  margin: 0;
}
.ctms-login-links a {
  color: var(--main);
}
/* "Signing in as {email}" under the MFA headings: pulled up against the
   heading so it reads as its subtitle (net ≈0.375rem with the card gap). */
.ctms-mfa-account {
  color: var(--text-muted);
  font-size: 0.8125rem;
  overflow-wrap: anywhere;
  margin: 0;
  margin-block-start: -0.875rem;
}
/* Status icon in a tinted circle on outcome pages (reset sent/done, Google
   reject). Tones come from the semantic status tokens (§3.1). */
.ctms-auth-hero {
  align-self: center;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ctms-auth-hero > i {
  font-size: 1.75rem;
}
.ctms-auth-hero.hero-success {
  background: color-mix(in srgb, var(--success) 12%, transparent);
  color: var(--success);
}
.ctms-auth-hero.hero-info {
  background: color-mix(in srgb, var(--info) 12%, transparent);
  color: var(--info);
}
.ctms-auth-hero.hero-neutral {
  background: color-mix(in srgb, var(--neutral) 12%, transparent);
  color: var(--neutral);
}
/* The 6-digit TOTP input: big centered digits are the focus of the MFA step.
   The trailing letter-spacing after the last digit is recentered by an equal
   text-indent. */
.field:has(> #mfa-code) {
  block-size: 4rem;
}
#mfa-code {
  block-size: 100%;
  font-size: 1.625rem;
  font-weight: 500;
  text-align: center;
  letter-spacing: 0.35em;
  text-indent: 0.35em;
  font-variant-numeric: tabular-nums;
}

/* MFA enrollment (M0.6): QR + manual seed + recovery codes. The QR PNG keeps
   its white quiet zone in both themes — scanners need the contrast. */
.ctms-mfa-qr {
  align-self: center;
  border-radius: 8px;
  background: #fff;
}
.ctms-mfa-manual {
  font-size: 0.8125rem;
  text-align: center;
}
.ctms-mfa-manual summary {
  color: var(--main);
  cursor: pointer;
}
.ctms-mfa-secret {
  display: block;
  margin-top: 0.5rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.08em;
  overflow-wrap: anywhere;
}
.ctms-mfa-manual-hint {
  color: var(--text-muted);
  margin-top: 0.25rem;
}
.ctms-recovery-codes {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem 1rem;
  list-style: none;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  background: var(--hover);
}
.ctms-recovery-codes code {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
}

/* Enrollment as numbered steps (scan → confirm). The numerals come from a CSS
   counter so nothing ordinal needs translating. Works inside both hosts: the
   auth card and Settings → Security. */
.ctms-mfa-steps {
  list-style: none;
  counter-reset: mfa-step;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
.ctms-mfa-steps > li {
  counter-increment: mfa-step;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.ctms-mfa-step-title {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-size: 0.875rem;
  font-weight: 600;
  margin: 0;
}
.ctms-mfa-step-title::before {
  content: counter(mfa-step);
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--selected);
  color: var(--main);
  font-size: 0.8125rem;
  font-weight: 600;
}

/* Copy/print actions under the one-time recovery codes. */
.ctms-codes-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
}
.ctms-security-narrow .ctms-codes-actions {
  justify-content: flex-start;
}

/* .ctms-print-area groups what a printed recovery-code sheet should contain
   (heading + intro + codes). On screen it's a plain column matching the card's
   internal rhythm; @media print at the end of this section isolates it. */
.ctms-print-area {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Settings → Security: reading-width column (§2.4) with the M3-ish scale
   section headings (§3.3). */
.ctms-security-narrow {
  max-width: 480px;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  align-items: flex-start;
}
.ctms-security-narrow form {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  align-items: flex-start;
}
.ctms-security-narrow .ctms-mfa-qr {
  align-self: flex-start;
}
/* In the settings column everything is left-aligned; the auth card centers. */
.ctms-security-narrow .ctms-mfa-manual {
  text-align: left;
}
.ctms-section-heading {
  font-size: 1rem;
  font-weight: 600;
  margin-top: 0.5rem;
}

/* Spinner on a disabled full-page-submit button (app.js adds the class on
   submit). ::before, because BeerCSS owns button::after for the ripple layer;
   currentColor keeps it legible on filled and outlined buttons alike. */
button.ctms-btn-loading::before {
  content: "";
  width: 1rem;
  height: 1rem;
  flex: none;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
  animation: ctms-spin 0.8s linear infinite;
}
@keyframes ctms-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Offscreen host for the clipboard-fallback textarea (app.js); keeps inline
   styles out of the JS. */
.ctms-offscreen {
  position: fixed;
  inset: auto auto 0 0;
  opacity: 0;
  pointer-events: none;
}

/* Printing a recovery-codes screen yields a clean sheet: only the
   .ctms-print-area (heading + intro + codes) is visible. visibility (not
   display) so the hidden layout doesn't reflow the printable part away. */
@media print {
  body:has(.ctms-print-area) * {
    visibility: hidden;
  }
  body:has(.ctms-print-area) .ctms-print-area,
  body:has(.ctms-print-area) .ctms-print-area * {
    visibility: visible;
  }
  body:has(.ctms-print-area) .ctms-print-area {
    position: absolute;
    inset: 0 auto auto 0;
    width: 100%;
  }
}

/* ---------- 16. Shared component styling ---------- */

/* Toasts: bottom-left, auto-dismiss handled in app.js (§4.5). */
#toasts {
  position: fixed;
  left: var(--ctms-gap);
  bottom: var(--ctms-gap);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 40;
  max-width: 24rem;
}
/* The auth layout has no footer, so the bare --ctms-gap is its air from the
   viewport edge. In the shell the bottom edge is the footer's top (§2.6), and
   without this a toast floats 8px above the hairline instead of the 56px every
   other floating thing keeps. */
body.ctms-shell #toasts {
  bottom: calc(var(--ctms-gap) + var(--ctms-footer-height));
}
.ctms-toast {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  background: var(--text);
  color: var(--card);
  border-radius: 8px;
  padding: 0.75rem 1rem;
  box-shadow: var(--elevate2);
}
.ctms-toast.toast-error {
  background: var(--error-bg);
  color: var(--error);
}
.ctms-toast > i {
  flex: none;
}

/* Global navigation progress bar (§5.3): htmx toggles .htmx-indicator opacity. */
#nav-progress {
  position: fixed;
  top: var(--ctms-navbar-height);
  left: 0;
  right: 0;
  width: 100%;
  height: 3px;
  z-index: 30;
}

/* Data tables (§4.2): borderless — no card chrome; the table sits directly on
   the page and structure comes from spacing, a muted header, and whisper-thin
   row hairlines. */
.ctms-table-card {
  /* No fill: --card is reserved for actual card components (§3.1, the report
     cards). A table is page content, so it sits directly on --page and reads as
     a table by spacing and hairlines alone. The sticky header therefore keeps
     the inherited --sticky-bg (--page) rather than overriding it. */
  border-radius: 12px;
  overflow: auto;
  /* The containing block for the in-flight loader DataTable mounts here
     (§16b). Nothing else positions against the card. */
  position: relative;
}
/* In-flight state (§5.3): pagination/filter requests point hx-indicator at
   the card; htmx stamps .htmx-request on it while loading. The dim is now the
   COMPANION of the four-dot loader DataTable centres over the card (§16b), not
   the whole signal — but it is the half that survives prefers-reduced-motion,
   and the half you see wherever on a long table you happen to be looking.

   THE DIM IS ON THE CHILDREN, NOT THE CARD. opacity applies to the whole
   subtree, so dimming the card would take the loader down with the rows it is
   supposed to stand out from — the mark would sit at the same 0.55 as the text
   behind it and the push-back would buy nothing. Everything but the overlay
   fades; the overlay stays at full strength. */
.ctms-table-card.htmx-request > :not(.ctms-loader-overlay) {
  opacity: 0.55;
  transition: opacity 0.15s;
}
.ctms-table-card table {
  width: 100%;
  font-size: 0.875rem;
}
.ctms-table-card :is(th, td) {
  padding: 0.875rem 1rem;
}
.ctms-table-card thead th {
  position: sticky;
  top: 0;
  background: var(--sticky-bg); /* covers scrolled rows; matches the page */
  z-index: 1;
  color: var(--text-muted);
  font-weight: 500;
  font-size: 0.8125rem;
  border-block-end: 0.0625rem solid var(--hairline);
}
.ctms-table-card tbody td {
  border-block-end: 0.0625rem solid var(--hairline);
}
/* A ROW IS ITS OWN LINK (§4.2). app.js forwards a click anywhere in the row to
   the row's identity link; the pointer is the promise that it will, so this
   selector is the handler's rule spelled in CSS and the two must move together.
   A link to a path of this app in one of the two IDENTITY cells, not drawn as a
   control (.button, [download]) and not filed under the trailing
   .ctms-row-actions — the same tests, in the same order. A row that fails them
   keeps the arrow and stays inert: the agenda-hours weekday, whose links are
   several ranges and one "add", is exactly the row that must not open something
   when clicked. */
table[data-row-links] tbody tr:has(> td:nth-child(-n + 2) a[href^="/"]:not(.button):not([download]):not(.ctms-row-actions a)) {
  cursor: pointer;
}
.ctms-table-card tbody tr:hover {
  /* The schema's own hover surface (§3.0.1) — a NEUTRAL step, not a primary
     wash. Row hover is "you are pointing at this", not "this is selected"; the
     accent-tinted --selected is what means selected. */
  background: var(--hover);
}

/* A CONTINUATION row: free text that belongs to the row above it rather than
   to a column of its own (M12.5's supply movements — the reason a quantity was
   discarded, and the note taken when it was spent). The hairline moves to the
   bottom of this row so the pair reads as one entry, and the row above loses
   its own; the type steps down because it is an aside, not a datum. */
.ctms-table-card tbody tr:has(+ tr.ctms-row-note) td {
  border-block-end: 0;
}
.ctms-table-card tbody tr.ctms-row-note td {
  padding-block-start: 0;
  font-size: 0.8125rem;
}

.ctms-table-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 1rem;
}

/* Empty state (§4.2). */
.ctms-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  padding: 48px 24px;
  text-align: center;
  color: var(--text-muted);
}
.ctms-empty-state > i {
  /* BeerCSS sizes icons via --_size (font-size AND the clipping box together);
     overriding font-size alone leaves the 1.5rem box and clips the glyph. */
  --_size: 3rem;
  color: var(--line-strong);
}

/* Sticky form footer for long forms (§4.3).

   OPAQUE, on the page background — the same treatment §4.2 gives the sticky
   table header, and for the same reason: what scrolls under a sticky bar has to
   disappear under it. A translucent fill (this was 92% + a backdrop blur) leaves
   every border and every glyph behind the bar showing at 8%, which on a long
   eSOURCE form put a textarea's edge and a file-drop outline straight through
   Cancel and Save. A blur cannot rescue that — a hairline reduced to 8% is still
   a crisp hairline — and with an opaque fill there is no backdrop left to blur. */
.ctms-form-footer {
  position: sticky;
  bottom: 0;
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
  padding: 0.75rem 0;
  background: var(--sticky-bg);
  /* Above BeerCSS field internals — the same reason #sidebar carries one, and
     BeerCSS's own value for a sticky bar. A .field is `position: relative` with
     `z-index: auto`, so it is NOT a stacking context: its select (z-index 1) and
     its caret (z-index 10) are painted in the nearest ancestor's context, where
     any positive z-index outranks a bar sitting at auto no matter how late it
     comes in the DOM. That is what put a select's text, its underline and its
     caret on top of Cancel and Save. 11 clears the field internals and stays
     under BeerCSS's menus (13) and dialogs (100), which must open OVER the bar. */
  z-index: 11;
  border-top: 0.0625rem solid var(--line);
}

/* Inline banners (§4.5) — borderless, tint-only (flat design). */
.ctms-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  border-radius: 8px;
  padding: 0.75rem 1rem;
  border: none;
  background: var(--hover);
}
/* The fill is the status's own -bg token, never a fresh mix of the TEXT colour
   (§3.1): the pair's tint is a brighter hue step, so re-mixing the text here
   left the warning banner washed-out cream while its chips read yellow. */
.ctms-banner.banner-error {
  background: var(--error-bg);
  color: var(--error);
}
.ctms-banner.banner-warning {
  background: var(--warning-bg);
  color: var(--warning);
}
.ctms-banner.banner-info {
  background: var(--info-bg);
  color: var(--info);
}

/* Page header row: title + status chip on one baseline (detail pages). */
.ctms-page-header {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.ctms-page-header .page-title {
  margin: 0;
}

/* One person-row in a detail card (root users on the site page): identity on
   the left, status chip + action on the right, hairline-separated. */
.ctms-root-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0;
  border-block-end: 0.0625rem solid var(--line);
}
.ctms-root-row:last-child {
  border-block-end: none;
}
.ctms-root-row > div:first-child {
  flex: 1;
  min-width: 0;
}
.ctms-root-row p {
  margin: 0;
}
.ctms-root-row form {
  margin: 0;
}

/* Users management (M1.2). Filter bar: filters left, primary action right. */
.ctms-filter-bar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.ctms-filter-form {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 0;
  /* As a flex item of the filter bar the form must be allowed to shrink —
     otherwise it sizes to its unwrapped content and runs past the viewport
     on narrow screens instead of wrapping its fields. */
  min-width: 0;
  max-width: 100%;
}
.ctms-filter-form .field {
  min-width: 14rem;
  /* BeerCSS's sibling-spacing rule adds margin-block-start to any .field that
     follows a sibling inside a non-.grid/nav/.row parent (the form); its
     :not()/:is() compound outweighs two plain classes, so !important is
     required — same as .ctms-logout-form above. */
  margin-block: 0 !important;
}
/* Date-range fields (M1.3 access logs) are content-sized: five 14rem fields
   overflow the 1366px content area; a date needs far less anyway. */
.ctms-filter-form .field:has(input[type="date"]) {
  min-width: 9.5rem;
}

/* A money cell (M9.3 invoiceables): the amount and its currency code are one
   token and must not break across two lines — a column of amounts is read by
   scanning down it, which a wrapped "1250.00 / USD" defeats. */
.ctms-money {
  white-space: nowrap;
}

/* What a filtered list ADDS UP TO (M9.3 invoiceables): one line per currency,
   above the table it summarizes. Flat and borderless like everything else —
   the numbers are the emphasis, not a box around them — and wrapping, because
   a site running three currencies gets three of these. */
.ctms-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

/* Account-type toggle: picking "staff" or "monitor" hides the site-only
   fields, and only "monitor" shows the initial-study block — pure CSS, no JS
   (the server enforces the pairing regardless). With no account_type inputs
   at all (the edit form), neither :has matches and everything site-only
   stays. */
.ctms-radio-row {
  display: flex;
  gap: 1.5rem;
}
.ctms-users-form:has(input[name="account_type"][value="staff"]:checked) .ctms-site-only,
.ctms-users-form:has(input[name="account_type"][value="monitor"]:checked) .ctms-site-only,
.ctms-monitor-only {
  display: none;
}
.ctms-users-form:has(input[name="account_type"][value="monitor"]:checked) .ctms-monitor-only {
  display: block;
}

/* A monitor's add-assignment form: select and button on one line, the same
   filter-bar alignment language. Pharmacy's unblinding grant (M8.4) reuses it
   with a third control — the typed study-code confirmation — so the row wraps
   rather than crushing the fields on a narrow screen. */
.ctms-assignment-add {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 1rem;
}

/* The masked investigational product line (M8.4, decision 4): study code,
   "Investigational Product", and the unit/drug reference — never a name. Reads
   as one phrase, with the crossed-eye icon carrying the same meaning as the
   blinded chip for anyone scanning rather than reading. */
.ctms-masked-ip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--text-muted);
}
.ctms-masked-ip > i {
  font-size: 1.125rem;
}
.ctms-assignment-add .field {
  flex: 1;
  max-width: 24rem;
}

/* Permission editor grid (§3.1 module × section × level): same borderless
   hairline language as the data tables; section rows indent under their
   module. */
.ctms-perm-grid {
  width: 100%;
  font-size: 0.875rem;
}
.ctms-perm-grid :is(th, td) {
  padding: 0.5rem 0.75rem;
  text-align: start;
}
.ctms-perm-grid thead th {
  color: var(--text-muted);
  font-weight: 500;
  font-size: 0.8125rem;
  border-block-end: 0.0625rem solid var(--hairline);
}
.ctms-perm-grid tbody td {
  border-block-end: 0.0625rem solid var(--hairline);
}
.ctms-perm-grid td:last-child {
  width: 13rem;
}
.ctms-perm-grid td p {
  margin: 0;
}
.ctms-perm-section td:first-child {
  padding-inline-start: 2rem;
  color: var(--text-muted);
}
.ctms-perm-select {
  margin-block: 0;
}

/* Selects are click-to-open controls: pointer cursor on the closed control
   in every browser — BeerCSS's `.field > :is(...) { all: unset }` strips the
   affordance otherwise (FRONTEND.md §10 checklist: cursor affordance). */
.field > select {
  cursor: pointer;
}

/* The date-input label rule that used to live here is gone: components.FormField
   now carries a blank placeholder on EVERY input, which makes BeerCSS's own
   `[placeholder]:not(:placeholder-shown) + label` float the label for a date or
   time input permanently — and, the reason it was added, for any field the
   server filled. One rule, in the component, instead of one per input type. */

/* Native <select> pickers — customizable-select, a CSS-only progressive
   enhancement: supporting browsers (Chromium) open the same flat panel and
   rounded tinted options as the navbar dropdowns; the rest keep the OS
   popup. No JS — keyboard, form semantics, and a11y stay native (§7). */
@supports (appearance: base-select) {
  /* .field > select outweighs BeerCSS's `.field > :is(...) { all: unset }`,
     which would otherwise reset appearance back to none. */
  select,
  .field > select,
  select::picker(select) {
    appearance: base-select;
  }
  select::picker(select) {
    background: var(--card);
    border: none;
    border-radius: 14px;
    box-shadow: 0 8px 28px light-dark(rgba(45, 55, 90, 0.18), rgba(0, 0, 0, 0.6));
    padding: 0.5rem;
    margin-block-start: 0.25rem;
  }
  select::picker-icon {
    display: none; /* the .field wrapper already overlays arrow_drop_down */
  }
  /* .field > select > option outweighs BeerCSS's same-specificity
     `background-color: var(--surface)` — rest-state options are bare. */
  select option,
  .field > select > option {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    min-height: 2.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: 8px;
    font-size: 0.875rem;
    color: var(--text);
    background: transparent;
    cursor: pointer;
  }
  select option:is(:hover, :focus-visible) {
    background: var(--hover);
  }
  select option:checked {
    background: var(--selected);
  }
  select option::checkmark {
    order: 1; /* trailing check: text stays left, check hugs the right edge */
    margin-inline-start: auto;
    color: var(--main); /* selected cue beyond tint alone (§3.1 spirit) */
  }
}

/* Usage & Monitoring (M1.3). Error detail/stack blocks: preformatted but
   wrapping, scrolling inside their own box — never the page (§4.2 spirit). */
.ctms-error-pre {
  margin: 0;
  padding: 12px 16px;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 40vh;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text-muted);
}
.ctms-error-stack > summary {
  cursor: pointer;
  list-style: disclosure-closed;
  margin-block-end: 8px;
}
.ctms-error-stack[open] > summary {
  list-style: disclosure-open;
}

/* Study catalogs (M2.1). Tab bar: BeerCSS tabs on the page background — flat,
   hairline under the row, primary underline on the active tab (its default
   look already matches the flat direction; just guarantee the cursor per
   checklist item 14 — anchors without href would lose it). */
.ctms-catalog-tabs a {
  cursor: pointer;
}

/* File drop zone: the REAL file input is the focusable element (native
   keyboard/SR path), visually merged into its label-zone. Dashed hairline,
   primary wash while a file hovers ([data-file-drop] toggles .dragover). */
.ctms-file-drop-field {
  margin-block-start: 1rem;
}
.ctms-file-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.375rem;
  padding: 1.5rem 1rem;
  border: 0.125rem dashed var(--line);
  border-radius: 12px;
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.ctms-file-drop > i {
  color: var(--main);
}
.ctms-file-drop-field.dragover .ctms-file-drop,
.ctms-file-drop:hover {
  border-color: var(--main);
  background: var(--hover);
}
.ctms-file-drop-field.invalid .ctms-file-drop {
  border-color: var(--error);
}
/* Keep the input in the accessibility tree and focus order, but let the zone
   be its visual body; focus shows on the zone via :focus-within. */
.ctms-file-drop input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  cursor: pointer;
}
.ctms-file-drop:focus-within {
  outline: 0.125rem solid var(--main);
  outline-offset: 0.125rem;
}
.ctms-file-drop-name {
  font-size: 0.875rem;
  color: var(--text-muted);
  word-break: break-word;
}
.ctms-file-drop-name:empty {
  display: none;
}
.ctms-file-drop-field > output {
  display: block;
  font-size: 0.75rem;
  padding: 0.25rem 1rem 0;
  color: var(--text-muted);
}
.ctms-file-drop-field > output.invalid {
  color: var(--error);
}

/* Color: swatch tile + hex code side by side — the value never rides on
   color alone (§3.1). The tile's background is data-driven inline style. */
.ctms-color-cell {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.ctms-color-swatch {
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 6px;
  border: 0.0625rem solid var(--line);
  flex: none;
}
/* Color form field: OUTSIDE the BeerCSS .field (its reset erases a color
   input's native swatch via appearance:none, and a floating label has no
   value text to float over). Label above, swatch well + live hex readout. */
.ctms-color-field {
  margin-block-start: 1rem;
}
.ctms-color-field > label {
  display: block;
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-block-end: 0.375rem;
}
.ctms-color-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* BeerCSS turns a color input into an invisible inset-0 overlay of its
   wrapper (opacity 0, absolute) — the well below is the visible control, its
   background carrying the picked value (data-driven inline style; app.js
   keeps it and the hex readout in sync). */
.ctms-color-well {
  position: relative;
  display: inline-block;
  inline-size: 3.5rem;
  block-size: 2.5rem;
  border: 0.0625rem solid var(--line);
  border-radius: 8px;
  flex: none;
}
.ctms-color-well:focus-within {
  outline: 0.125rem solid var(--main);
  outline-offset: 0.125rem;
}
.ctms-color-well input[type="color"] {
  cursor: pointer; /* checklist item 14: the overlay IS the click target */
}
.ctms-color-field > output {
  display: block;
  font-size: 0.75rem;
  padding-block-start: 0.25rem;
  color: var(--text-muted);
}
.ctms-color-field > output.invalid,
.ctms-color-field.invalid > label {
  color: var(--error);
}

/* Logo thumbnails in tables and the edit form's current-logo row. */
.ctms-logo-thumb {
  max-height: 2.5rem;
  max-width: 6rem;
  object-fit: contain;
  vertical-align: middle;
}
.ctms-current-logo {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-block-start: 1rem;
}

/* ---------- 16b. The four-dot loader (§3.0.2 exception, §5.3) ---------- */

/* THE LOGO IS THE SPINNER. The app waits often — a lazy chart, a page of a
   table — and every one of those moments used to be a stock BeerCSS ring whose
   track colour was a hardcoded grey. This is the delivered four-dot mark with
   the accent walking clockwise around it instead (owner direction + reference
   frames, Aug-2026). §3.0.2's "never rebuild it in CSS, never recolor the dots"
   and §3.0.4's closed accent list were amended in the same change to carry this
   one exception; both were rewritten, not quietly broken. The markup lives in
   components/loader.templ and nowhere else.

   Only the FILL travels. The dots never move, scale or rotate, so the mark is
   never redrawn — it is the logo with one property animated. */

/* The travelling dot. Every path runs THIS keyframe; only the phase differs
   (below), so there is one description of the motion rather than four.

   The trail behind the leader is free: 0% is the dot lit, and interpolating
   back to --main across the following 40% of the cycle IS the fade the
   reference frames show under the dot that has just moved on. 40%→90% is the
   ink plateau, and 90%→100% is the quick ramp back up. 0% and 100% are the same
   colour, which is what makes the loop seamless. */
@keyframes ctms-loader-dot {
  0% {
    fill: var(--accent);
  }
  40% {
    fill: var(--main);
  }
  90% {
    fill: var(--main);
  }
  100% {
    fill: var(--accent);
  }
}

.ctms-loader {
  /* Geometry only, per the NAMING RULE. The two sizes are BeerCSS's own
     progress.circle (2.5rem) and .circle.small (1.5rem), so replacing a ring
     with the mark is dimensionally a no-op wherever one stood. */
  --ctms-loader-size: 2.5rem;
  display: block;
  flex: none;
  width: var(--ctms-loader-size);
  height: var(--ctms-loader-size);
}
/* NOT `.small`. BeerCSS sizes bare SVGs by class —
   `:is(img,svg,video):is(.small,.medium,.large,.tiny,.extra,.round,.circle,
   .square,.responsive)` sets its own --_size and, crucially, `inline-size`,
   which is `width` under another name at a specificity our `.ctms-loader` does
   not reach. A `.small` mark silently renders at BeerCSS's size instead of
   ours. Same family of trap as its token names: grep beer.min.css before
   naming anything on an element it styles. */
.ctms-loader-small {
  --ctms-loader-size: 1.5rem;
}

/* BeerCSS's global *{border-radius:inherit} bleeds an ancestor radius into an
   inline SVG and deforms it — the same trap .ctms-brand-lockup resets at §11
   and .ctms-brand-mono repeats at §14b. A loader dropped inside a rounded card
   would otherwise render as four squashed lozenges. */
.ctms-loader,
.ctms-loader path {
  border-radius: 0;
}

/* THE RESTING FRAME IS THE LOGO, and that is load-bearing rather than tidy.
   §19 kills every animation under prefers-reduced-motion with !important, so
   these declared fills are exactly what a reduced-motion user sees: the brand
   mark sitting still, accent at bottom-right, never a half-finished frame. Do
   not exempt this from §19 — for those users the block's own 0.55 dim (a static
   property; only its transition is killed) and role="status" carry the message. */
.ctms-loader path {
  fill: var(--main);
  animation: ctms-loader-dot 1.2s linear infinite;
}
.ctms-loader path:nth-child(4) {
  fill: var(--accent);
}

/* THE PHASE, AND WHY EVERY DELAY IS NEGATIVE. Path order in the delivered asset
   is top-left, top-right, bottom-left, bottom-right; the accent travels
   CLOCKWISE from its home corner — BR → BL → TL → TR — one step every 300ms.
   A positive delay would hold a dot on its 0% frame (accent) until it started,
   so the mark would open with two lit dots; a negative one starts it already
   part-way through, and at t=0 the three ink dots are all on the plateau.
   Transposing these two lines reverses the direction — verify against the
   reference frames, not by eye. */
.ctms-loader path:nth-child(1) {
  animation-delay: -0.6s; /* top-left, lights 3rd */
}
.ctms-loader path:nth-child(2) {
  animation-delay: -0.3s; /* top-right, lights 4th */
}
.ctms-loader path:nth-child(3) {
  animation-delay: -0.9s; /* bottom-left, lights 2nd */
}
.ctms-loader path:nth-child(4) {
  animation-delay: 0s; /* bottom-right, the home corner, lights 1st */
}

/* The IN-PLACE form: a lazy placeholder saying what it is fetching. A row and
   not a centred block, so the chart or table arriving in its place does not make
   the page jump by the height of a spinner sitting in the middle of nothing. */
.ctms-loader-block {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-block: 1rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* The IN-FLIGHT form: the mark centred over a block reloading in place. It is
   in the DOM at all times and revealed by the .htmx-request class htmx stamps
   on the element the request's hx-indicator names, so it needs no JS and costs
   one hidden div. pointer-events:none because the block underneath is dimmed,
   not disabled — hx-disabled-elt handles the control that fired the request. */
.ctms-loader-overlay {
  position: absolute;
  inset: 0;
  display: none;
  place-items: center;
  pointer-events: none;
  z-index: 2; /* over the sticky thead (§16, z-index 1) it is centred across */
}
.htmx-request > .ctms-loader-overlay {
  display: grid;
}

/* ---------- 17. Responsive (§8) ---------- */

/* Tablet (768–1199px): nothing to force — the rail is always collapsed and
   hover-expansion is irrelevant without a pointer (§8). */

/* Mobile: off-canvas sidebar drawer + compact navbar. */
@media (max-width: 767px) {
  body {
    --ctms-navbar-height: 56px;
    /* Desktop's 60px buys breathing room; on a phone the same air costs a
       fifth of the fold, and #main-content's own 16px padding already gives
       the title its gap under the bar. */
    --ctms-breadcrumbs-height: 48px;
    --ctms-content-offset: 0px;
    --ctms-footer-height: 44px;
  }
  .ctms-footer {
    padding: 0 16px;
  }
  /* A phone bar seats two short items, no more. The strapline is the
     informational half — the copyright and build are what a user is ever asked
     to read back on a support call — so it goes. The copyright drops to the
     short cut for the same reason. */
  .ctms-footer-compliance,
  .ctms-footer-copy {
    display: none;
  }
  .ctms-footer-copy-short {
    display: inline;
  }
  /* The margin follows the right-hand group's first SURVIVING member. */
  .ctms-footer-version {
    margin-inline-start: auto;
  }
  /* Three items do not fit a phone bar. The strapline is the one that is
     informational rather than identifying, so it is the one that goes; the
     copyright and build stay because they are what a user is ever asked to
     read back during a support call. */
  .ctms-footer-compliance {
    display: none;
  }
  .ctms-footer-legal {
    margin-inline-start: auto;
  }
  #main-content {
    padding: 16px;
  }
  .ctms-navbar {
  padding: 0 2rem 0 1rem;
  }
  #breadcrumbs {
    padding: 0 16px;
  }
  #sidebar {
    left: 0;
    height: 70vh; /* floating card drawer, not full-height */
    /* Keep auto vertical margins (as the desktop rail) so the 70vh card centers
       between the navbar and the bottom inset; margin:0 would over-constrain
       top+bottom+height and pin the card to the top. */
    margin: auto 0;
    border-radius: 0 20px 20px 0;
    transform: translateX(-110%);
    transition: transform 0.2s ease;
    width: 256px;
  }
  body.sidebar-open #sidebar {
    transform: translateX(0);
  }
  body.sidebar-open .ctms-sidebar-label {
    display: block;
  }
  body.sidebar-open .ctms-sidebar-item {
    justify-content: flex-start;
    padding: 0 1rem 0 1.75rem;
  }
  .ctms-sidebar-lock {
    display: none; /* the drawer always shows labels; hover-lock is pointless */
  }
  .ctms-sidebar-account {
    /* the relocated navbar controls surface in the drawer footer (§8) */
    display: inline-flex;
    align-self: center;
    margin-top: 0.5rem;
  }
  .ctms-menu-button {
    display: inline-flex;
  }
  /* Right-side controls collapse into the sidebar drawer's settings popup (§8). */
  .ctms-navbar .ctms-user-name,
  .ctms-navbar .ctms-navbar-wide {
    display: none;
  }
  /* Filter fields stack full-width: side-by-side 14rem fields either overflow
     or leave awkward half-rows on a phone. */
  .ctms-filter-form .field {
    flex: 1 1 100%;
    min-width: 0;
  }
  /* Drop the "Mimir <FantasyName>" half of the lockup — wordmark SVG and accent
     suffix together — and keep only the four-dot mark so the brand uses minimal
     width. The module dropdown still carries the active module name (§8). */
  .ctms-brand-lockup .ctms-wordmark {
    display: none;
  }
  /* Move the logo mark to the navbar's right edge: order it after the .max
     spacer so it's pushed to the far right. DOM/tab order is unchanged (the
     brand still comes first for assistive tech). The height is pinned rather
     than inherited: the desktop 28px is sized so the suffix beside it stays
     legible, and with the suffix gone the bare mark must still not crowd the
     56px mobile bar — the two happen to agree today, so this holds the mobile
     end steady if the desktop lockup is ever resized again. */
  .ctms-navbar .ctms-brand-lockup {
    order: 1;
    --ctms-brand-height: 1.75rem;
  }
}

/* ---------- 17b. Studies (M2.2) ---------- */

/* Study/protocol codes render copy-friendly and tabular (§4.2). */
.ctms-id {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 0.875rem;
}

/* Vendor-rows editor: a plain group (no fieldset chrome — flat design), each
   row = two selects + a remove button aligned to the fields' input line. */
.ctms-vendor-rows {
  border: none;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.ctms-vendor-rows > legend {
  padding: 0;
  font-size: 1rem;
  font-weight: 600;
  margin-block-end: 0.75rem;
}
.ctms-vendor-rows > output.invalid {
  display: block;
  color: var(--error);
  font-size: 0.75rem;
  margin-block-end: 0.5rem;
}
.ctms-vendor-row {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: 0.75rem;
  align-items: start;
}
/* BeerCSS drops the first field's top margin; pin both selects (and the
   remove button) to one input line. */
.ctms-vendor-row > .field {
  margin-block-start: 1rem;
}
.ctms-vendor-row .ctms-vendor-remove {
  margin-block-start: 1.25rem;
}

/* Lifecycle state form: select + apply button on one line. */
.ctms-state-form {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
}
.ctms-state-form .field {
  flex: 1;
  max-width: 20rem;
}
.ctms-state-form button {
  margin-block-start: 0.25rem;
}

/* ---------- 17c. Delegation Log (M2.3) ---------- */

/* The viewer's active-today delegation chips (the visible face of the authz
   delegation tags, §3.2). */
.ctms-delegation-today {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* The page's action row (it outgrew the delegation log: every list toolbar uses
   it). NEVER put `.no-space` on the element that carries this — BeerCSS ships
   `:is(nav,.row,li).no-space{gap:0}`, two class selectors deep, so it outranks
   this single-class gap no matter how late custom.css loads, and the buttons
   silently render edge to edge (the toolbar in the Aug-2026 spacing pass). The
   same trap eats `.ctms-invoice-actions`, `.ctms-query-act-row` and
   `.ctms-filter-bar`; a bare `<nav>` already gaps at 1rem, so there is nothing
   to switch off. */
.ctms-delegation-actions {
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* Voided rows stay visible in the DoA log — struck-through and de-emphasized;
   archived study documents shown via the include-archived toggle get the same
   treatment (.ctms-row-struck). The status chip carries the non-color signal
   (§3.1) and is not struck. */
.ctms-delegation-voided td,
.ctms-row-struck td {
  text-decoration: line-through;
  color: var(--text-muted);
}
.ctms-delegation-voided td .chip,
.ctms-delegation-voided td a,
.ctms-row-struck td .chip,
.ctms-row-struck td a {
  text-decoration: line-through;
}
.ctms-delegation-voided td .chip,
.ctms-row-struck td .chip {
  text-decoration: none;
}

/* Per-version view/download links in the document tables: compact inline
   anchors. The download is a plain <a> on purpose — its route 302s to the
   storage host, never an HTMX swap. */
.ctms-doc-download {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  white-space: nowrap;
}
.ctms-doc-download i {
  font-size: 1.25rem;
}
.ctms-doc-download + .ctms-doc-download {
  margin-inline-start: 0.75rem;
}

/* In-app document viewer: the embed fills the content width and most of the
   viewport height, separated by a hairline (flat design — no card chrome).
   PDFs iframe the browser's viewer; images cap to the frame's proportions. */
.ctms-doc-viewer {
  display: block;
  width: 100%;
  height: calc(100vh - var(--ctms-navbar-height) - 16rem);
  min-height: 24rem;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.ctms-doc-viewer-image {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - var(--ctms-navbar-height) - 16rem);
  border-radius: 12px;
}

/* Viewer popup (owner direction, Jul-2026): a near-fullscreen modal makes
   better use of the screen than the in-flow page. Column layout: header row
   (title/meta + actions), then the embed stretches into the leftover height
   (min-height: 0 lets the iframe shrink inside the flex column). The
   element+class selector outweighs BeerCSS's dialog{} defaults (10% top,
   80% max height); BeerCSS's own transform rules keep the open/close
   animation. */
dialog.ctms-viewer-dialog {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  inset: 3vh auto auto 50%;
  inline-size: 96vw;
  block-size: 94vh;
  max-inline-size: none;
  max-block-size: none;
  padding: 0.5rem 1rem 1rem;
}
/* One compact row: title · chip · meta, actions pushed right. The meta line
   absorbs the leftover width and ellipsizes so the row never wraps taller. */
.ctms-viewer-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}
.ctms-viewer-head .section-heading {
  margin: 0;
  white-space: nowrap;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ctms-viewer-head p {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ctms-viewer-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  white-space: nowrap;
  margin-inline-start: auto;
}
.ctms-viewer-dialog .ctms-doc-viewer {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
}
.ctms-viewer-imgwrap {
  display: flex;
  justify-content: center;
}
.ctms-viewer-dialog .ctms-viewer-imgwrap {
  flex: 1 1 auto;
  min-height: 0;
}
.ctms-viewer-dialog .ctms-doc-viewer-image {
  max-height: 100%;
  object-fit: contain;
}
.ctms-viewer-dialog .ctms-empty-state {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
@media (max-width: 767px) {
  dialog.ctms-viewer-dialog {
    inset: 0 auto auto 50%;
    inline-size: 100vw;
    block-size: 100dvh;
    border-radius: 0;
  }
}

/* Visit-schedule section heading (M3.2): the section name and its archived
   chip on the left, the reorder arrows and rename/archive actions pushed
   right. Wraps rather than overflowing at tablet widths. */
.ctms-schedule-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.ctms-schedule-head .section-heading {
  margin: 0;
  margin-inline-end: auto;
}

/* Start-up checklist row actions (M2.5): the per-row transition buttons sit
   inline; nowrap keeps the bounded checklist scannable at 1366px.

   It goes on a WRAPPER INSIDE the cell, never on the <td> itself: display is
   inline-flex, and an inline-flex <td> stops being a table cell — the browser
   wraps it in an anonymous cell that no rule can reach, so the row's hairline
   (drawn per-td, §4.2) breaks under that column and the stub paints at the
   wrong height. Invisible while every row is one line tall; the moment a cell
   grows to two lines it is a visible gap. */
.ctms-row-actions {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  white-space: nowrap;
}

/* Start-up stage reorder arrows (M2.5): two icon-button mini-forms side by
   side inside the table cell. Forms because the swap is a plain server
   round-trip; BeerCSS gives forms block layout + margins, which would stack
   the arrows and stretch the row. */
.ctms-reorder-cell {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}
.ctms-reorder-cell form {
  display: inline-flex;
  margin: 0;
}
.ctms-reorder-cell button i {
  font-size: 1.25rem;
}

/* eSOURCE Builder canvas — the fields an input list repeats (M3.5a). Nesting is
   ONE level deep, so the cue is a single indent plus a hairline spine on the
   left of the first cell: structure from spacing and hairlines, never from card
   chrome or a nested table (§4.2). The row keeps its own hover and hairlines. */
.ctms-nested-row > td:first-child {
  padding-inline-start: 2rem;
  position: relative;
}
.ctms-nested-row > td:first-child::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0.75rem;
  border-inline-start: 0.0625rem solid var(--line);
}

/* Checklist item 14, found by reading the COMPUTED cursor rather than the CSS:
   BeerCSS gives a .radio/.checkbox label a pointer but leaves the real <input>
   inside it at the browser default — so the affordance vanishes over the
   control itself, which is the one pixel a user aims at. Every choice group in
   the app is this markup (M4.4's search filters, M1.2's account type, M7.2's
   eSOURCE forms), so the fix belongs here rather than in any one of them. */
label.radio > input:not(:disabled),
label.checkbox > input:not(:disabled) {
  cursor: pointer;
}

/* The file field's accepted-types checkboxes (M3.5a): a plain group, laid out
   like a form field so its legend and helper line up with the labels and helper
   text of the inputs above and below it (§4.3). */
.ctms-checkbox-group {
  border: none;
  margin: 0 0 1rem;
  padding: 0;
}
.ctms-checkbox-group > legend {
  padding: 0;
  margin-bottom: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
/* Same cue an outlined field gets when it is refused: the label turns
   error-colored, never the error text alone (§4.3). */
.ctms-checkbox-group.invalid > legend {
  color: var(--error);
}
.ctms-checkbox-group-options {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}
/* An option's label WRAPS. BeerCSS sets white-space: nowrap on .checkbox and on
   its span, which is invisible for "Femenino" and overflows the grid cell for a
   sentence like "Only participants carrying a flag" — pushing the search panel
   past the content box, which is the one thing §8 forbids (tables scroll inside
   their card; the page never scrolls sideways). Found in a browser at an
   effective 760px, not in review. */
.ctms-checkbox-group-options .checkbox,
.ctms-checkbox-group-options .checkbox > span {
  white-space: normal;
}
.ctms-checkbox-group > output {
  display: block;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-checkbox-group > output.invalid {
  color: var(--error);
}

/* The calculation field's insert pickers (M3.5b): a select and its Insert
   button on one line, the button aligned to the select's own input row rather
   than to the block, so it does not ride up onto the floating label or down
   onto the helper text. Wraps to two lines on a narrow screen, where a button
   beside a select stops being readable. */
.ctms-formula-picker {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.ctms-formula-picker > .field {
  flex: 1 1 16rem;
  margin-bottom: 1rem;
}
.ctms-formula-picker > button {
  margin-block-start: 0.5rem;
}

/* The parameters of a catalogued formula: one select per input, grouped so it
   reads as "these belong to the formula chosen above". Structure from SPACING
   — an indent plus the legend — the way .ctms-checkbox-group groups the file
   categories; no box, no border (§4.2). */
.ctms-formula-params {
  border: none;
  margin: 0 0 1rem;
  padding: 0 0 0 1rem;
}
.ctms-formula-params > legend {
  padding: 0;
  margin-bottom: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* The field-capability block (M3.6). A heading, not a card: the capabilities
   are a second group of settings on the same form, and the app separates page
   sections with SPACING and a hairline rather than chrome (§3, §4.2). */
.ctms-form-section {
  margin: 1.5rem 0 1rem;
  padding-top: 1rem;
  border-top: 0.0625rem solid var(--line);
  font-size: 1rem;
  font-weight: 600;
}

/* The visibility condition: operator, value and the no-JS refresh button on
   one line, laid out exactly like the formula pickers so the two controls that
   compose something server-side look like one another. */
.ctms-condition-row {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.ctms-condition-row > .field {
  flex: 1 1 12rem;
  margin-bottom: 1rem;
}
.ctms-condition-row > button {
  margin-block-start: 0.5rem;
}

/* One status select per answer the field can give, grouped by indent and
   legend — .ctms-formula-params' treatment, for the same reason: these belong
   to the answers listed above them. */
.ctms-status-triggers {
  border: none;
  margin: 0 0 1rem;
  padding: 0 0 0 1rem;
}
.ctms-status-triggers > legend {
  padding: 0;
  margin-bottom: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-status-triggers > output {
  display: block;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-status-triggers > output.invalid {
  color: var(--error);
}

/* Capability chips in the canvas table's details column: they follow the
   kind's own summary line, so each gets its own row rather than trailing the
   sentence. */
.ctms-capability-chip {
  margin-top: 0.25rem;
}

/* Derived and referenced values on the subject profile (M4.1): age, BMI, race,
   ethnicity — read-only, so they are a definition list rather than disabled
   inputs, which would look editable and are not. Structure comes from spacing,
   not from a card or a border (§3, §4.2). */
.ctms-derived-list {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin: 0 0 0.75rem;
}
.ctms-derived-list dt {
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-derived-list dd {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
}

/* Subject → Medical History (M4.2 + amendment). Four categories are tables of
   records, allergies are a chip row; each section is an HTMX swap target of its
   own. Flat like everything else: sections are separated by space and one
   hairline, never by a card (§3, §4.2). */
.ctms-history-category {
  padding-block-end: 1rem;
  margin-block-end: 1.5rem;
  border-block-end: 1px solid var(--line);
}
.ctms-history-category:last-of-type {
  border-block-end: none;
}
/* The picker sits under a table, so it keeps a readable measure of its own
   rather than stretching to the table's width. */
.ctms-history-category .ctms-history-picker {
  max-width: 32rem;
}
.ctms-history-empty {
  margin: 0 0 0.5rem;
}
/* Both margins explicit: BeerCSS drops the top margin of a first child, which
   on a grid means column one starts ~10px above its four neighbours. */
.ctms-history-category > .section-heading {
  margin-block: 0 0.75rem;
}
/* In-flight state (§5.3): an add or a removal points hx-indicator at the
   category it will swap (the .ctms-table-card convention). */
.ctms-history-category.htmx-request {
  opacity: 0.55;
  transition: opacity 0.15s;
}
.ctms-history-terms {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
  padding: 0;
  list-style: none;
}
/* One recorded term: the chip, its archived marker when the catalog entry was
   deactivated, and the remove button — one hit area each, no hover-only
   affordance (§8: tablets have no hover). */
.ctms-history-term {
  display: inline-flex;
  align-items: center;
  gap: 0.125rem;
}
/* The remove control is secondary to the term it sits beside: chip-height,
   ink in the reduced-emphasis color, and only tinted on hover/focus. Sized
   explicitly because BeerCSS's .circle.small is still taller than a chip. */
.ctms-history-term > button {
  inline-size: 1.5rem;
  block-size: 1.5rem;
  min-inline-size: 1.5rem;
  padding: 0;
  color: var(--text-muted);
}
.ctms-history-term > button > i {
  font-size: 1rem;
}
.ctms-history-term > button:is(:hover, :focus-visible) {
  color: var(--error);
}
.ctms-history-picker {
  margin-block-start: 0.75rem;
}
.ctms-history-picker .field {
  margin-block-end: 0.5rem;
}
/* The results panel doubles as its own loading indicator: it dims while the
   debounced search is in flight instead of adding a spinner beside the box. */
.ctms-history-options {
  transition: opacity 0.15s;
}
.ctms-history-options.htmx-request {
  opacity: 0.5;
}
.ctms-history-options p {
  margin: 0.25rem 0;
  font-size: 0.8125rem;
}
.ctms-history-option-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ctms-history-option-list form,
.ctms-history-inline {
  margin: 0;
}
.ctms-history-inline {
  margin-block-start: 0.5rem;
}

/* A row's provenance (M15.1). The badge saying WHERE the assertion was made is
   a chip; this is the line under it naming the study, the visit and the form.
   Helper-text size and secondary colour because it qualifies the term rather
   than competing with it (the .ctms-column-note shape). */
.ctms-history-origin {
  display: block;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* The lock banner (M15.1): why this tab is read-only, and which studies closed
   it. The studies are a LIST beside the sentence rather than words inside it
   (§7), so they need their own rhythm; the banner owns the colour. */
.ctms-history-lock-title {
  margin: 0;
  font-weight: 600;
}
.ctms-history-lock-text {
  margin: 0.25rem 0 0;
}
.ctms-history-lock-studies {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
}
.ctms-history-lock-studies li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}


/* The record form's partial date: three controls in one labelled group, so
   "day unknown, month March, year 2014" is one field with one error. A
   fieldset because that is what a group of controls with one legend is. */
.ctms-date-parts {
  margin-block: 1rem 0.5rem;
  padding: 0;
  border: none;
}
.ctms-date-parts > legend {
  padding: 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-date-parts-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block-start: 0.5rem;
}
.ctms-date-parts-row > .field {
  flex: 1 1 6rem;
}
/* The year is a number, not a list: it needs no room for month names. */
.ctms-date-parts-row > .field:last-child {
  flex: 1 1 7rem;
}
/* The row carries BeerCSS's `row` class for ONE reason: their block-sibling
   rule — :not(.grid,nav,.row) > :not(progress.max) + :is(…,.field,…) — gives
   every .field after the first a 1rem top margin, which left Día sitting 16px
   above Mes and Año. A .row parent is the library's own opt-out, and at (0,4,1)
   that rule is not worth out-specifying. What `row` also brings and this row
   does not want is re-asserted below (custom.css loads after beer.min.css, so
   equal specificity resolves our way): labels must be free to wrap, and the gap
   is the group's own. */
.ctms-date-parts-row {
  white-space: normal;
  gap: 0.75rem;
}
.ctms-date-parts > output {
  display: block;
  margin-block-start: 0.25rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-date-parts > output.invalid,
.ctms-date-parts.invalid > legend {
  color: var(--error);
}
/* Ticking "still ongoing" removes the end date rather than contradicting it
   (the .ctms-users-form :has precedent — CSS only, no JS). */
.ctms-history-ongoing {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-block-start: 0.5rem;
}
/* The name is matched by SUFFIX because the same fieldset is drawn twice: bare
   on the Medical History tab, and prefixed per block inside an eSOURCE record
   (M15.4), where a page may hold five of them and every control carries its
   field's id. */
.ctms-history-period:has(input[name$="ongoing"]:checked) .ctms-history-end {
  display: none;
}

/* Advanced search (M4.4). The panel is flat like everything else (§3.0.5): it
   separates from the list by spacing and a single hairline above the actions,
   never by a card, a fill or a shadow. */
.ctms-filter {
  margin: 0;
}
.ctms-search-panel {
  margin-block-start: 1rem;
}
.ctms-search-panel > .grid {
  margin-block-end: 0.5rem;
}
/* The criteria group is a fieldset because that is what a labelled group of
   controls is; its border would be the app's only boxed form region. */
.ctms-search-criteria {
  border: none;
  margin: 0;
  padding: 0;
}
.ctms-search-criteria > legend {
  padding: 0;
  margin-block-end: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-criteria-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
  padding: 0;
  list-style: none;
}
/* A criterion reads as one object: label and its remove control share a tonal
   pill, the treatment selected options already carry (§4.8). */
.ctms-criterion {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.125rem 0.25rem 0.125rem 0.75rem;
  border-radius: 1rem;
  background: var(--selected);
  color: var(--main);
  font-size: 0.8125rem;
}
.ctms-criterion > button {
  color: inherit;
}
.ctms-criteria-picker-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: flex-start;
}
.ctms-criteria-picker-controls > .field {
  flex: 1 1 12rem;
  min-width: 0;
  /* BeerCSS's block-sibling rule adds a top margin to every .field after the
     first inside a non-.grid/nav/.row parent; its compound :not()/:is()
     selector outweighs a single class (the .ctms-filter-form precedent). */
  margin-block: 0 !important;
}
.ctms-criteria-picker > .ctms-history-options {
  margin-block-start: 0.5rem;
}
.ctms-search-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  justify-content: flex-end;
  margin-block-start: 1rem;
  padding-block-start: 1rem;
  border-block-start: 1px solid var(--line);
}

/* Subjects → Admin → duplicate review queue (M4.5). Each queued row is a
   CARD and not a table row, because the decision is made by reading the
   uploaded row against the participants it looks like, and a table cell
   cannot hold both sides of that comparison.

   Flat, per §3.0.5 and the flat-borderless direction: the cards are separated
   by SPACING and one whisper-thin hairline apiece — no border box, no
   elevation, no fill. The hairline is the data table's, mixed the same way,
   so the queue and the tables above it read as one surface. */
.ctms-card-list {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
/* <article> is a filled, rounded BeerCSS surface out of the box; the reset is
   what makes it flat. Same reasoning as the data table's "borderless — no card
   chrome" (§4.2): the queue sits directly on the page background. */
.ctms-flat-card {
  margin: 0;
  padding: 0;
  padding-block-end: 1.5rem;
  background: none;
  box-shadow: none;
  border: none;
  border-radius: 0;
  border-block-end: 0.0625rem solid var(--hairline);
}
/* The last card needs no separator: there is nothing below it to separate. */
.ctms-flat-card:last-child {
  padding-block-end: 0;
  border-block-end: none;
}
.ctms-card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-block-end: 0.25rem;
}
.ctms-card-head h3 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
}
/* The three resolutions. Each is its own <form> — three different decisions,
   not three values of one control — so the row is laid out here rather than
   by a shared parent form. */
.ctms-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.ctms-card-actions form {
  margin: 0;
}
.ctms-plain-list {
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  font-size: 0.875rem;
}

/* Print-only header of the printed DoA sheet; voided rows drop from paper
   (owner decision — the printable log is the formal document). */
.ctms-print-only {
  display: none;
}

@media print {
  .ctms-print-only {
    display: block;
  }
  .ctms-print-area tr.ctms-delegation-voided {
    display: none;
  }
}

/* ---------- 17d. Recruitment pipeline (M5.2) ---------- */

/* The pipeline's buckets are a filter row that CARRIES the counts — the
   grouped-list answer to a drag-and-drop board (owner decision Jul-2026). Flat
   like everything else (§3.0.5): the row separates from the table by spacing,
   never by a card or a fill.

   The selected chip is BeerCSS's .fill plus a leading check icon and
   aria-current, so the selection survives §3.1's rule that state is never
   conveyed by color alone. */
.ctms-pipeline-buckets {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}
/* BeerCSS gives .chip no pointer even as an <a>, because its own chips are
   often static labels (checklist item 14 — verify the computed cursor, not the
   CSS you wrote). These are links, so they say so. */
.ctms-pipeline-buckets > .chip {
  cursor: pointer;
}
/* The count reads as a value attached to the label, not as part of it: a
   tonal capsule at the chip's trailing edge, tabular so a column of them does
   not jitter as the numbers change. */
.ctms-bucket-count {
  margin-inline-start: 0.375rem;
  padding-inline: 0.4rem;
  border-radius: 1rem;
  background: color-mix(in srgb, currentcolor 12%, transparent);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* Agenda closures (M6.1a). A closure is either a WHOLE day or part of one, so
   the time fields are irrelevant while "whole day" is ticked. Hiding them is
   CSS-only via :has() on the form — the .ctms-users-form staff/site rule above
   set this precedent, and it beats both a <script> and an HTMX round trip for
   what is a checkbox. Where :has() is unsupported the fields simply stay
   visible; they carry no `required`, and the server ignores them whenever
   whole_day is set, so nothing depends on the CSS. */
.ctms-closure-form:has(#agenda-closure-whole-day:checked) .ctms-closure-times {
  display: none;
}

/* The study scope: a borderless fieldset (§4.2 — structure from spacing, not
   chrome) whose legend reads as a field label like every other in the form. */
.ctms-closure-scope,
.ctms-check-fieldset {
  margin: 1rem 0 0;
  padding: 0;
  border: 0;
}
.ctms-closure-scope > legend,
.ctms-check-fieldset > legend {
  padding: 0;
  color: var(--text);
  font-size: 0.875rem;
  font-weight: 600;
}
.ctms-closure-scope > .checkbox,
.ctms-check-fieldset > .checkbox {
  display: flex;
  margin-block-start: 0.5rem;
}

/* Scheduling screen (M6.2). The controls sit on one line at desktop widths and
   wrap at tablet ones — a visit picker, a day and a length are read together,
   and a coordinator changing the day should not have to scroll to see the
   grid change. */
.ctms-schedule-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: start;
  /* The rows' own spacing, now that the fields carry no margin of their own:
     the visit/day line and the duration/transport line below it are two of
     these containers, and without this they would sit flush. */
  margin-block-start: 1rem;
}
.ctms-schedule-controls > .field {
  flex: 1 1 14rem;
  min-width: 0;
  /* BOTH margins, and !important for the same reason .ctms-filter-form states:
     BeerCSS adds margin-block-start to any .field FOLLOWING a sibling, and its
     :not()/:is() compound outweighs two plain classes. Left alone, the day
     field of this row sat 16px below the visit picker beside it. The row's own
     gap is what spaces the controls. */
  margin-block: 0 !important;
}
/* The checkbox aligns with the FIELDS, not with their labels: BeerCSS inputs
   are 3rem tall and a bare checkbox would float at the top of the row. */
.ctms-schedule-transport {
  display: flex;
  align-items: center;
  min-height: 3rem;
}

/* The availability grid: rooms across, the site's own clock down. It scrolls
   INSIDE its card rather than widening the page (§8 — the body never scrolls
   horizontally), and the time column stays put while it does, because a cell
   with no hour beside it says nothing. */
.ctms-agenda-grid {
  min-width: 32rem;
}
.ctms-agenda-grid th[scope="row"] {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  background: var(--sticky-bg);
  font-weight: 400;
  text-align: start;
  white-space: nowrap;
}
/* A GRID is read by cell, not by row (owner direction Jul-2026). The shared
   table-card row hover is right where a row is the thing you click — the
   registry, the studies list — and wrong here: nothing selects a whole hour
   across every room, so the hover belongs on the square a coordinator can
   actually press, and a row lighting up beside it only competes with it. Same
   specificity as the rule it cancels, and after it, so it wins. */
.ctms-agenda-grid tbody tr:hover,
.ctms-calendar-week tbody tr:hover,
.ctms-calendar-month tbody tr:hover {
  background: transparent;
}

/* One cell. Every state is a filled capsule of the same size, so the grid
   reads as a map at a glance and the eye lands on the free ones — and every
   state carries a WORD (§3.1: never colour alone). */
.ctms-slot {
  display: block;
  /* The cell's own padding stays INSIDE its 100% width. BeerCSS's button reset
     leaves a free slot at content-box, so `width: 100%` plus the padding spilled
     over the table cell's trailing padding — every block sat flush against its
     right neighbour and 16px away from its left one, which is the asymmetry the
     grid showed. Stating the box model here makes the free slots and the booked
     block measure the same way. */
  box-sizing: border-box;
  width: 100%;
  padding: 0.35rem 0.5rem;
  border: 0;
  border-radius: 8px;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  color: var(--text-muted);
  font-size: 0.8125rem;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The free cell is the screen's one action and the only element here that is
   pressable — hence the pointer (checklist item 14: BeerCSS's field reset and
   the browser's own button default both strip it). */
button.ctms-slot-free {
  cursor: pointer;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  font-weight: 600;
  transition: background 150ms ease;
}
button.ctms-slot-free:hover,
button.ctms-slot-free:focus-visible {
  background: color-mix(in srgb, var(--accent) 24%, transparent);
}
/* ONE box per booking (owner direction Jul-2026): the busy block fills the
   cell it spans, so a visit that occupies three rows of the grid is one block
   the height of the visit rather than three chips with gaps between them. Its
   leading edge carries the STUDY's colour, set in eTrials → Admin (inline
   style, data-driven) — never the text's colour, and never the only signal:
   the block names the participant and the hours it covers. */
.ctms-slot-busy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.125rem;
  block-size: 100%;
  border-inline-start: 0.1875rem solid var(--line-strong);
  background: color-mix(in srgb, var(--info) 16%, transparent);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.ctms-slot-when {
  color: var(--text-muted);
  font-size: 0.6875rem;
}
.ctms-slot-who {
  font-weight: 600;
}
/* The participation being booked, so a wall of occupied cells is legible as
   "this is already them" rather than as a full day. */
.ctms-slot-own {
  background: color-mix(in srgb, var(--success) 22%, transparent);
}
.ctms-slot-off {
  background: transparent;
  color: color-mix(in srgb, var(--text-muted) 60%, transparent);
}

/* One appointment inside a visit-schedule row (M6.3). A visit can carry
   several — "booked three times, missed twice" — so each one is its own line
   of when / where / what became of it, wrapping rather than widening the
   table. */
.ctms-plan-appointment {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
}
.ctms-plan-appointment + .ctms-plan-appointment {
  margin-block-start: 0.5rem;
  padding-block-start: 0.5rem;
  border-block-start: 1px solid var(--line);
}

/* ---------- 17e. Calendar (M6.4) ---------- */

/* The three controls of the calendar screen — whose, which layout, which
   period — sit on one line while there is room and wrap as a whole rather than
   interleaving, because they are three independent questions. */
.ctms-calendar-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.5rem;
}
/* The strip's own gap is its ONLY spacing. BeerCSS adds margin-block-start to a
   <nav> that follows a sibling (the same rule the schedule controls fight, whose
   :is() list includes nav), which pushed the view switch 16px below the scope
   toggle beside it — each group then centred its chips on a different line. Its
   :not()/:is() compound outweighs a class plus a universal, hence !important. */
.ctms-calendar-controls > * {
  margin-block: 0 !important;
}
.ctms-calendar-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
/* ONE control height across the strip. The three groups are three different
   BeerCSS components — small chips are 32px, buttons 40px, a bordered field
   48px — and centring them on a common line left a ragged row of three sizes.
   40px is the app's control height (§3.4: buttons 36–40px, inputs 40px), so
   everything here measures that. */
.ctms-calendar-controls .chip {
  block-size: 2.5rem;
}
.ctms-calendar-jump .field {
  /* BeerCSS sizes a field from its own --_input custom property (3rem by
     default), so the height is set THERE rather than fought with block-size —
     2.5rem is the value its own `.small` fields use. */
  --_input: 2.5rem;
  /* Same BeerCSS sibling-spacing rule the schedule controls fight: this field
     follows the nav's buttons. */
  margin-block: 0 !important;
}
.ctms-calendar-nav .section-heading {
  min-inline-size: 12rem;
  text-align: center;
}
/* The jump-to-date control is the shared field component (BeerCSS renders a
   date input as an invisible overlay of its .field wrapper, so a bare input
   would be a control nobody can see), sized down to sit on the nav line. */
.ctms-calendar-jump {
  inline-size: 11rem;
}
.ctms-calendar-jump .field {
  margin-block: 0;
}

/* Every calendar grid is a table on the page background, structured by
   hairlines alone (§4.2 — borderless, no card chrome of its own). */
.ctms-calendar-day th,
.ctms-calendar-week th,
.ctms-calendar-month th {
  vertical-align: bottom;
}
.ctms-calendar-day td,
.ctms-calendar-week td,
.ctms-calendar-month td {
  vertical-align: top;
}
.ctms-calendar-day,
.ctms-calendar-week,
.ctms-calendar-month {
  inline-size: 100%;
  min-inline-size: 44rem;
  /* Fixed layout so the columns are the site's rooms in equal share rather
     than a ranking of whichever one happens to hold the longest entry. */
  table-layout: fixed;
}
.ctms-calendar-day th:first-child,
.ctms-calendar-day td:first-child {
  inline-size: 5rem;
}
.ctms-calendar-day td,
.ctms-calendar-week td,
.ctms-calendar-month td {
  text-align: start;
}
.ctms-calendar-week th,
.ctms-calendar-month th {
  text-align: start;
}
.ctms-calendar-week th > span {
  display: block;
}
/* Hours the site is shut, and the neighbouring-month days a monthly grid draws
   to keep its weeks whole: present, legible, and visibly not part of the
   working period. */
.ctms-calendar-closed > td,
.ctms-calendar-closed > th,
.ctms-calendar-outside {
  background: color-mix(in srgb, var(--text) 4%, transparent);
}
.ctms-calendar-outside .ctms-calendar-daynum {
  color: color-mix(in srgb, var(--text-muted) 60%, transparent);
}
/* Today, in the one place FRONTEND.md §3.1 names --tertiary for. */
.ctms-calendar-today {
  box-shadow: inset 0.1875rem 0 0 var(--main);
}
.ctms-calendar-daynum {
  display: inline-block;
  min-inline-size: 1.5rem;
  margin-block-end: 0.25rem;
  border-radius: 8px;
  color: var(--text);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  cursor: pointer; /* checklist item 14: the day number opens the day */
}
.ctms-calendar-daynum:hover,
.ctms-calendar-daynum:focus-visible {
  background: var(--hover);
  color: var(--main);
}
.ctms-calendar-daylist {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
/* Day cells keep a floor height so the week and month read as a calendar
   rather than as a one-line table that happens to have seven columns — and so
   the closed/outside shading has something to shade. */
.ctms-calendar-week td,
.ctms-calendar-month td {
  block-size: 5.5rem;
}

/* One booked thing. Read-only by design — the calendar creates nothing
   (calendar.md) — so it is a quiet block that either links through to the
   participation's schedule or does not. */
.ctms-calendar-entry {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: flex-start;
  /* BeerCSS centres the content of an <a>; an entry reads left-to-right like
     every other row of data in the app. */
  justify-content: flex-start;
  gap: 0.25rem 0.375rem;
  padding: 0.25rem 0.5rem;
  /* ONE box per appointment: the block fills the cell it spans, so a 60-minute
     visit on a 30-minute axis is one tall block rather than two chips with a
     gap between them (owner direction Jul-2026). */
  block-size: 100%;
  border-radius: 8px;
  /* The study's colour lands on this edge (inline style, data-driven). */
  border-inline-start: 0.1875rem solid var(--line-strong);
  background: color-mix(in srgb, var(--text) 6%, transparent);
  color: var(--text);
  font-size: 0.75rem;
  line-height: 1.25;
}
/* In the daily view the cell IS the block, so the block fills it: the classic
   table trick — a 1px cell height makes the row's real height (set by the time
   column) the definite height a percentage resolves against, so a visit
   spanning three rows renders as one box three rows tall. */
.ctms-calendar-day td,
.ctms-agenda-grid td {
  block-size: 0.0625rem;
  padding-block: 0.125rem;
}
a.ctms-calendar-entry {
  cursor: pointer; /* checklist item 14 */
  transition: background 150ms ease;
}
a.ctms-calendar-entry:hover,
a.ctms-calendar-entry:focus-visible {
  background: var(--hover);
}
.ctms-calendar-entry + .ctms-calendar-entry {
  margin-block-start: 0.25rem;
}
.ctms-calendar-when {
  color: var(--text-muted);
}
.ctms-calendar-what {
  font-weight: 600;
}

/* eSOURCE → a FORM: one page of a visit record, filled (M7.2).

   FRONTEND.md §4.3's "Long forms (eSOURCE!)" case, so the form is capped to a
   reading measure rather than stretched across a data-table width: a column of
   controls a coordinator reads top to bottom, with the sticky footer
   (.ctms-form-footer, already shared) holding Cancel and Save. */
.ctms-record-form {
  max-inline-size: 55rem;
}

/* One field's block, and the ONLY thing that separates one control from the
   next on this page.

   Every top-level field is wrapped in the id its structural acts swap
   (FieldBlock, §5.1), and a bare wrapper is not something BeerCSS spaces: its
   block rule is `… + :is(…, .field, fieldset, p, …)`, which matches only when
   the control ITSELF is the sibling. One level down, a field got no margin at
   all, and the only thing left between two controls was the helper line under
   the first — so a field WITHOUT helper text sat flush against the next one's
   label, and an outlined M3 label is drawn THROUGH whatever it lands on (the
   date above the time, the dropdown above the catalog search). Spacing belongs
   to the block, so it is the same 1rem whatever kind the block holds; the
   per-kind bottom margins are dropped here for exactly that reason — they were
   each compensating for this in their own units, which is why a checklist and a
   date field parted company by different amounts. */
.ctms-record-form > .ctms-field-block {
  margin-block-end: 1rem;
}
.ctms-field-block > .ctms-checkbox-group,
.ctms-field-block > .ctms-term-picker,
.ctms-field-block > .ctms-pending-field,
.ctms-field-block > .ctms-history-field,
.ctms-field-block > .ctms-file-field,
.ctms-field-block > .ctms-signature-field {
  margin-block-end: 0;
}

/* A field of one of the four kinds M7.3/M7.4 will fill. It is a STATEMENT, not
   a control: the page must show every field it holds, and a disabled input
   would read as something that could be answered. Same hairline separation the
   rest of the flat design uses — no card, no shadow (§3.0.5). */
.ctms-pending-field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-block-end: 1rem;
  padding-block: 0.75rem;
  border-block-start: 0.0625rem solid var(--line);
}
.ctms-pending-label {
  font-weight: 600;
}
.ctms-pending-field > p {
  flex-basis: 100%;
  margin: 0;
  font-size: 0.75rem;
}

/* The MEDICAL HISTORY block (M15.3): the participant's own conditions,
   medications or allergies drawn inside the study's record. Like the pending
   field above it is a STATEMENT and not a control — but what it states is a
   TABLE, so it stacks instead of laying its parts out in a row. Same hairline
   separation, so a page of fields keeps one rhythm whatever a field holds. */
.ctms-history-field {
  display: block;
  margin-block-end: 1rem;
  padding-block: 0.75rem;
  border-block-start: 0.0625rem solid var(--line);
}
.ctms-history-field > .ctms-pending-label {
  display: block;
  margin-block-end: 0.5rem;
}
.ctms-history-field > p {
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
}
/* The row UNDER a struck row saying who withdrew it, and the one holding its
   monitor threads. Neither is struck — a note about a withdrawal is not itself
   withdrawn, which is the .ctms-signature-line rule one screen along. */
.ctms-history-row-note > td,
.ctms-history-row-queries > td {
  padding-block-start: 0;
  text-decoration: none;
  font-size: 0.75rem;
}

/* One term of a chip-row category (allergies) INSIDE a record's block: the
   tab's compact chip row, with room under it for the withdrawal note and the
   monitor thread a term can carry — both of which want the full width the
   inline row does not give. */
.ctms-history-block-term {
  flex-wrap: wrap;
}
.ctms-history-block-term > .ctms-query-thread,
.ctms-history-block-term > .ctms-query-open,
.ctms-history-block-term > .text-secondary {
  flex-basis: 100%;
}

/* A block that is being swapped by one of its own sub-actions dims while the
   request is in flight — the .ctms-table-card treatment, on the wrapper the
   swap actually targets (§5.1). Every block-level act on this screen points its
   hx-indicator here: adding a list entry, withdrawing a file, and from M15.4
   recording a term on the participant. The button also disables itself, but a
   block that changes SIZE when it comes back needs the block itself to say it
   is working. */
.ctms-field-block.htmx-request {
  opacity: 0.55;
  transition: opacity 0.15s;
}

/* The block's WRITE face (M15.4): the picker that adds a term, and the editor
   that records what it says. Both sit under the table they act on, separated
   by space alone — a card around a form inside a form would be two boxes deep
   on a page that is already one long column (§3.0.5). */
.ctms-history-block-picker {
  margin-block-start: 0.75rem;
}
.ctms-history-block-picker .field {
  margin-block-end: 0.5rem;
}
.ctms-history-block-picker p {
  margin: 0.25rem 0;
  font-size: 0.8125rem;
}
/* The search box and its button on one line, the .ctms-term-search shape the
   searchable list already uses — declared there and reused here. */
.ctms-history-editor {
  margin-block-start: 0.75rem;
  padding-block-start: 0.75rem;
  border-block-start: 0.0625rem solid var(--line);
}
.ctms-history-editor > .section-heading {
  margin-block: 0 0.75rem;
}
/* The editor's own two buttons. Right-aligned like every form footer in the
   app, and NOT sticky: this is a form inside a page that already has one, and
   a second bar pinned to the viewport would compete with the page's Save. */
.ctms-history-editor-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-block-start: 0.75rem;
}

/* Struck-through content that is NOT a table row: a withdrawn list entry's
   heading, a withdrawn file's name, a voided signature on the printed export,
   a term the block is showing withdrawn. The row treatment is
   .ctms-row-struck above; this is the inline one, and it de-emphasizes for the
   same reason — the content stays because §4 keeps it, and it must not read as
   current. */
.ctms-struck {
  text-decoration: line-through;
  color: var(--text-muted);
}

/* A calculation's computed value (M7.4a). Tabular figures so a column of them
   lines up digit under digit, and the same weight as the label it answers —
   this is a VALUE the record holds, not a control somebody can reach. */
.ctms-calc-value {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* A note UNDER a control, in helper-text size: why a field is read-only for
   this viewer (M7.4a's role restriction), or that its value is computed. It
   sits where the helper text would, because that is where a reader looks for
   the reason a control behaves the way it does. */
.ctms-field-note {
  margin-block: -0.75rem 1rem;
  font-size: 0.75rem;
}

/* Searchable list (M3.1's inline expansion, second consumer): the catalog
   search and its button on one line, the answer select underneath. The button
   aligns to the input's own row rather than to the block, so it does not ride
   up onto the floating label — the .ctms-formula-picker rule, same problem. */
.ctms-term-picker {
  margin-block-end: 1rem;
}
.ctms-term-search {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.ctms-term-search > .field {
  flex: 1 1 18rem;
}
.ctms-term-search > button {
  margin-block-start: 0.5rem;
}

/* A number field drawn as a SCALE: its steps on one line, wrapping rather than
   scrolling the page sideways (§8 — the page never scrolls horizontally). */
.ctms-scale-row {
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
}

/* eSOURCE → the advanced fields at fill time (M7.3).

   An INPUT LIST is a group of groups, and the only structure it gets is
   spacing plus one hairline per repetition — no card, no shadow, no border box
   (§3.0.5, and the flat direction the whole app follows). The indent is what
   says "these controls belong to this entry", the same job .ctms-nested-row
   does on the Builder canvas one module along. */
.ctms-list-field,
.ctms-file-field {
  margin-block-end: 1.5rem;
  padding: 0;
  border: 0;
}
.ctms-list-field > legend,
.ctms-file-field > legend {
  padding-inline: 0;
  font-weight: 600;
}
.ctms-list-entry {
  margin-block: 1rem;
  padding-block-start: 0.75rem;
  padding-inline-start: 1rem;
  border-block-start: 0.0625rem solid var(--line);
  border-inline-start: 0.125rem solid var(--line);
}
.ctms-list-entry-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-block-end: 0.5rem;
}
.ctms-list-entry-head > .section-heading {
  margin: 0;
}
/* THE ADVERSE EVENT'S OWN ACT (M15.5b): what a repetition of a declaring list
   says about the participant's medical conditions, under the event's fields.
   Spaced off them, because it is a statement about the PROFILE and not another
   answer of this record — and the sentence it may carry instead of the button
   reads as one. */
.ctms-entry-promote {
  margin-block-start: 1rem;
}
.ctms-entry-promote > p {
  margin-block: 0.25rem 0;
}
/* The condition this event already made. An icon and a sentence rather than a
   chip: it is a fact about somewhere else, and a chip would read as a status
   of the entry itself. */
.ctms-entry-promote-note {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}
.ctms-entry-promote-note > i {
  font-size: 1.125rem;
  color: var(--main);
}

/* A withdrawn entry is still the record — it is dimmed, never hidden, and
   never removed from the flow (§4 + FRONTEND.md §9: empty ≠ removed). */
.ctms-list-entry.removed,
.ctms-file-row.removed {
  opacity: 0.65;
}
/* The wrapper is always rendered so an out-of-band swap has a target, and
   collapses when there is nothing withdrawn to offer. */
.ctms-removed-toggle {
  margin-block: 0 1rem;
}
.ctms-removed-toggle:empty {
  display: none;
}

/* One attached file: what it is, and what may be done with it, on one line
   that wraps rather than scrolls. */
.ctms-file-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  padding-block: 0.5rem;
  border-block-start: 0.0625rem solid var(--line);
}
.ctms-file-row > .ctms-row-actions {
  margin-inline-start: auto;
}
.ctms-file-upload {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block-start: 0.75rem;
}
.ctms-file-upload > .ctms-file-drop-field {
  flex: 1 1 18rem;
}

/* An electronic signature field before anyone has signed it: the meaning the
   template declares, quoted, and the state it is in. The quote rule is the one
   piece of chrome, because the text is what somebody will be attesting to. */
.ctms-signature-field {
  margin-block-end: 1.5rem;
  padding-block: 0.75rem;
  border-block-start: 0.0625rem solid var(--line);
}
.ctms-signature-meaning {
  margin: 0.5rem 0;
  padding-inline-start: 0.75rem;
  border-inline-start: 0.125rem solid var(--line);
  font-style: italic;
}
.ctms-signature-field > p {
  margin-block: 0.25rem 0;
  font-size: 0.75rem;
}


/* eSOURCE → the form LIFECYCLE (M7.5).

   One signature as the record holds it: who signed, under which specialty and
   licence, at which UTC instant. A superseded one is dimmed rather than hidden —
   the .ctms-list-entry `removed` rule one kind along — because a voided
   signature is still part of what the record says happened (§4). */
.ctms-signature-line {
  margin-block: 0.5rem 0;
  font-size: 0.8125rem;
}
.ctms-signature-line.removed {
  opacity: 0.65;
}
/* The strike starts BELOW the paragraph on purpose: text-decoration inherits
   into descendants and cannot be switched off by them, so striking the <p>
   would strike the note that says who voided it too. */
.ctms-signature-line.removed > .ctms-signature-who {
  text-decoration: line-through;
}
.ctms-signature-void {
  display: block;
}

/* The reason-for-change block, inside the answers form. Separated by the same
   hairline every other group on this screen uses, and capped to the form's own
   reading measure. */
.ctms-reason-block,
.ctms-lifecycle {
  max-inline-size: 55rem;
  margin-block: 1.5rem 0;
  padding-block-start: 0.75rem;
  border-block-start: 0.0625rem solid var(--line);
}
.ctms-reason-block > p,
.ctms-lifecycle > p,
.ctms-lifecycle form > p {
  margin-block: 0.25rem 0.75rem;
  font-size: 0.75rem;
}
.ctms-lifecycle form {
  margin-block-start: 0.75rem;
}

/* The signing ceremony (signature.templ): its own page, so it reads as a
   statement of what is about to be recorded rather than as a form to fill. */
.ctms-sign-form {
  max-inline-size: 40rem;
}
.ctms-sign-identity {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 1rem;
  margin-block: 1rem;
}
.ctms-sign-identity > dt {
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-sign-identity > dd {
  margin: 0;
  font-weight: 600;
}


/* ---------- 17f. eSOURCE Record view, Timeline & export sheet (M7.6) ---------- */

/* THE TIMELINE (M7.6, rows made expandable in M7.6a).

   A list of disclosures rather than a data table: a row is not a record to
   click through to, it is a thing to READ IN PLACE, and only a <details> opens
   without JavaScript. The grid below keeps the columns the table had, so it
   still scans like every other list in the app (§4.2's spacing-and-hairlines
   structure, no card chrome). */
.ctms-timeline {
  --ctms-timeline-cols: 12rem minmax(10rem, 1.4fr) minmax(9rem, 1fr) 8rem minmax(7rem, 0.8fr) 2.5rem;
}
.ctms-timeline-head,
.ctms-timeline-line {
  display: grid;
  grid-template-columns: var(--ctms-timeline-cols);
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.5rem 0.5rem 0;
}
.ctms-timeline-head {
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--text-muted);
  border-block-end: 0.0625rem solid var(--line);
}
.ctms-timeline-row {
  border-block-end: 0.0625rem solid var(--hairline);
}
.ctms-timeline-line {
  cursor: pointer;
  font-size: 0.875rem;
  /* The marker is the app's own chevron, on the left of the first cell, so the
     row still starts where the header says it does. */
  list-style: none;
  position: relative;
  padding-inline-start: 1.5rem;
}
.ctms-timeline-line::-webkit-details-marker {
  display: none;
}
.ctms-timeline-line::before {
  content: "chevron_right";
  font-family: "Material Symbols Rounded", sans-serif;
  font-size: 1.125rem;
  line-height: 1;
  position: absolute;
  inset-inline-start: 0;
  color: var(--text-muted);
  transition: transform 0.15s ease;
}
.ctms-timeline-row[open] > .ctms-timeline-line::before {
  transform: rotate(90deg);
}
.ctms-timeline-line:hover {
  background: var(--hover);
}
.ctms-timeline-row[open] > .ctms-timeline-line {
  font-weight: 500;
}
.ctms-timeline-when,
.ctms-timeline-visit {
  display: flex;
  flex-direction: column;
}
.ctms-timeline-title,
.ctms-timeline-who {
  overflow-wrap: anywhere;
}
/* Which clock a row was placed by, and the section a form sits in: both are
   context under the value, at the app's secondary size (§3.3). */
.ctms-timeline-clock {
  font-size: 0.75rem;
}
/* The pencil is the way from reading to writing. It is the row's only control,
   so it keeps its own hit area rather than inheriting the summary's. */
.ctms-timeline-edit {
  justify-self: end;
}

/* THE PANEL: one form's answers, read-only, fetched the first time the row is
   opened. It reuses the export's own table (14f below), indented under the line
   it belongs to. */
.ctms-timeline-panel {
  padding: 0.25rem 0 1rem 1.5rem;
  font-size: 0.875rem;
}
.ctms-timeline-panel.htmx-request {
  opacity: 0.55;
  transition: opacity 0.15s;
}
.ctms-timeline-panel .ctms-export-answers {
  margin-block-start: 0;
}
.ctms-timeline-panel-actions {
  margin-block-start: 0.75rem;
}
/* The no-JavaScript fallback: the disclosure still opens, onto the way in. */
.ctms-timeline-fallback {
  color: var(--main);
  cursor: pointer;
}

@media (max-width: 1199px) {
  /* The columns collapse to two: what and when on the left, the rest under it.
     Nothing is hidden — §8's one hard rule — it just stacks. */
  .ctms-timeline {
    --ctms-timeline-cols: 1fr auto;
  }
  .ctms-timeline-head {
    display: none;
  }
  .ctms-timeline-line {
    row-gap: 0.25rem;
  }
  .ctms-timeline-line > :not(.ctms-timeline-edit) {
    grid-column: 1;
  }
  .ctms-timeline-edit {
    grid-row: 1 / span 5;
    grid-column: 2;
  }
}

/* ---------- THE RECORD WORKSPACE (M7.6a) ----------

   The app's FIRST multi-pane screen: sections across the top, visits and their
   pages down the left, the form on the right. Everything below is layout only —
   the panes are flat on the page like every other data surface (§4.2), and the
   selector's item states borrow the sidebar's vocabulary (a left accent bar and
   a soft primary wash) at content scale, WITHOUT its neumorphism, which stays
   the sidebar's alone (§3.0.5). */

/* ONE strip above the workspace (owner direction, Jul-2026): what the screen is
   showing on the left — the section tabs and their two acts — and how to switch
   what it shows on the right, as icons. Two rows of labelled tabs and buttons
   cost this screen the one thing it exists to buy: the form, above the fold. */
.ctms-record-bar {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin-block-end: 2rem;
}
.ctms-record-bar-start {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex: 1 1 auto;
  min-width: 0;
  /* The tabs draw their own active underline on this same edge. */
  margin-block-end: -0.0625rem;
}
.ctms-record-bar-end {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex: none;
  padding-block: 0.25rem;
}
/* The section tabs size to their CONTENT and sit at the left, rather than
   stretching across the row the way a full-width tab strip does — the row now
   carries three groups, and equal thirds would put "Eventos adversos" in the
   middle of the screen. */
.ctms-section-tabs {
  flex: 0 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.ctms-section-tabs > a {
  /* BeerCSS gives every tab `inline-size: 100%` so a strip splits its row into
     equal thirds. Here the tabs are one of three groups on the row, so they
     size to their own label instead. */
  flex: 0 0 auto;
  inline-size: auto;
  padding-inline: 1rem;
  white-space: nowrap;
}
.ctms-record-section-actions {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex: none;
}

/* The two readings as a segmented control: one bordered group, the current
   reading tinted. Every half keeps aria-label + title, because an icon-only
   control without them is the state §7 forbids. */
.ctms-view-switch {
  display: inline-flex;
  align-items: center;
  border: 0.0625rem solid var(--line);
  border-radius: 999px;
  overflow: hidden;
}
.ctms-view-switch-tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  height: 2rem;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.ctms-view-switch-tab:hover {
  background: var(--hover);
}
.ctms-view-switch-tab.active {
  background: var(--selected);
  color: var(--main);
}
.ctms-view-switch-tab > i {
  font-size: 1.125rem;
}

.ctms-record-workspace {
  display: grid;
  /* minmax(0, 1fr) on the form column, not 1fr: a grid item's default min-width
     is auto, so one wide table inside the form would push the whole grid past
     the viewport instead of scrolling inside its own card. */
  grid-template-columns: 17rem minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

/* THE TREE IS STICKY, and #main-content stays the page's one scrollable region
   (§2.4). It scrolls inside itself only when a forty-visit protocol is taller
   than the viewport — the treatment the sidebar's own section list already has
   (§2.2), scrollbar hidden until hover. */
.ctms-record-tree {
  position: sticky;
  top: 0;
  max-height: calc(100dvh - var(--ctms-navbar-height) - var(--ctms-breadcrumbs-height) - var(--ctms-footer-height) - 2rem);
  overflow-y: auto;
  scrollbar-width: none;
  padding-block-end: 0.5rem;
}
.ctms-record-tree:hover {
  scrollbar-width: thin;
}
/* A <details> at every width, but the disclosure only MEANS anything at narrow
   ones (below): at desktop widths the summary is hidden and the tree is simply
   there — which is also why the element is rendered `open`, since CSS cannot
   open a closed one. One markup, CSS decides, and nothing hides behind hover
   (§8's one hard rule). */
.ctms-record-tree > summary {
  display: none;
}
/* BeerCSS styles a bare <nav> as a horizontal flex row (with `nav > * {flex:
   none}`), which laid the whole tree out sideways and clipped everything past
   the first visit. The landmark is right for a navigation tree, so the element
   stays and the layout is stated here — the same trap the subject tabs
   documented from the other side. */
.ctms-record-tree > nav {
  display: block;
}
.ctms-record-tree-empty {
  padding: 0.5rem 0.75rem;
}

.ctms-tree-visit {
  padding-block: 0.375rem;
  border-block-start: 0.0625rem solid var(--hairline);
}
.ctms-tree-visit:first-child {
  border-block-start: none;
}
.ctms-tree-node {
  display: flex;
  align-items: center;
  /* BeerCSS centres anchors inside a <nav>; a tree reads down its left edge. */
  justify-content: flex-start;
  text-align: start;
  gap: 0.5rem;
  padding: 0.375rem 0.75rem;
  border-radius: 8px;
  color: var(--text);
  cursor: pointer;
  /* The active state's accent bleeds from the left edge, so the bar has a
     place to sit on every item rather than only on the active one. */
  border-inline-start: 0.1875rem solid transparent;
  transition: background 0.15s ease;
}
/* The wash FADES to transparent, exactly as the sidebar's items do (§2.2): a
   flat fill of the same mix reads as a large saturated block in dark mode,
   which §3.2 asks us not to paint. */
.ctms-tree-node:hover {
  background: linear-gradient(90deg, var(--hover), transparent);
}
.ctms-tree-node.active {
  background: linear-gradient(90deg, var(--selected), transparent);
  border-inline-start-color: var(--main);
  font-weight: 600;
}
.ctms-tree-visit-name {
  font-weight: 600;
}
.ctms-tree-visit.active > .ctms-tree-visit-name {
  color: var(--main);
}
.ctms-tree-record {
  margin-inline-start: 0.75rem;
  font-size: 0.8125rem;
}
/* A page carries three facts and the column is 17rem wide, so they stack: the
   title on its own line, the progress and the lifecycle chip under it. */
.ctms-tree-page {
  margin-inline-start: 0.75rem;
  font-size: 0.8125rem;
  flex-wrap: wrap;
  row-gap: 0.125rem;
}
.ctms-tree-page-title {
  flex: 1 1 100%;
  min-width: 0;
  overflow-wrap: anywhere;
}
.ctms-tree-page-progress {
  font-size: 0.75rem;
}
/* "Open another occurrence": an act, not a place, so it reads as one — the
   primary colour the rest of the tree's items never carry at rest. */
.ctms-tree-open {
  margin-inline-start: 0.75rem;
  font-size: 0.8125rem;
  color: var(--main);
}
.ctms-tree-open > i {
  font-size: 1rem;
}
.ctms-tree-note {
  margin: 0;
  padding: 0 0.75rem 0.25rem 0.9375rem;
  font-size: 0.75rem;
}
.ctms-tree-flag {
  font-size: 1rem;
  color: var(--warning);
}

/* The record's provenance, one line above the form. */
.ctms-record-meta {
  margin-block: 0.25rem 0;
}

/* THE NARROW WIDTH (§8 has no rule for a two-column content layout; this is
   M7.6a's). The grid becomes one column and the tree collapses to its
   disclosure, whose summary names the current selection — so the form gets the
   full width and the selector is one click away rather than hidden. */
@media (max-width: 1199px) {
  .ctms-record-workspace {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
  .ctms-record-tree {
    position: static;
    /* OPEN, but bounded. The disclosure starts open because a closed <details>
       cannot be forced open by CSS at the desktop width where its summary is
       hidden — and a tree nobody can reach is worse than one that costs a
       scroll. The cap keeps the form on the first screenful either way, and the
       summary still collapses it for the rest of a long page. */
    max-height: 14rem;
    overflow-y: auto;
    border-block-end: 0.0625rem solid var(--line);
    padding-block-end: 0.75rem;
  }
  .ctms-record-tree:not([open]) {
    max-height: none;
    overflow: visible;
  }
  .ctms-record-tree > summary {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.25rem;
    cursor: pointer;
    font-weight: 600;
  }
  .ctms-record-tree-summary {
    overflow-wrap: anywhere;
  }
}

/* A workspace is not the export sheet — the document has its own print rules
   (below) — but somebody will press Ctrl+P on a form, and a pane stuck to the
   top of every sheet is not what they meant. */
@media print {
  .ctms-record-tree {
    position: static;
    max-height: none;
    overflow: visible;
  }
}

/* THE EXPORT SHEET.
   It carries its own class rather than .ctms-print-area (§13): that one escapes
   #main-content's overflow by going position:absolute, which is exactly right
   for a one-page recovery-code sheet and would clip a participant's record to
   page one. The rules below undo the shell's scroll model instead, so the
   document FLOWS across as many sheets as it needs. */
.ctms-export-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.ctms-export-sheet {
  display: block;
  margin-block-start: 1.5rem;
  max-width: 1100px;
}
.ctms-export-head {
  border-block-end: 1px solid var(--line);
  padding-block-end: 1rem;
  margin-block-end: 1.5rem;
}
.ctms-export-head > h2 {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.5rem;
}
.ctms-export-form {
  border-block-start: 1px solid var(--line);
  padding-block-start: 1.5rem;
  margin-block-start: 1.5rem;
}
.ctms-export-form:first-of-type {
  border-block-start: none;
  padding-block-start: 0;
  margin-block-start: 0;
}
.ctms-export-answers {
  width: 100%;
  border-collapse: collapse;
  margin-block-start: 1rem;
  font-size: 0.875rem;
}
.ctms-export-answers th,
.ctms-export-answers td {
  text-align: start;
  vertical-align: top;
  padding: 0.5rem 0.75rem 0.5rem 0;
  font-variant-numeric: tabular-nums;
}
.ctms-export-answers thead th {
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--text-muted);
  border-block-end: 1px solid var(--line);
}
.ctms-export-answers tbody tr.ctms-export-answer > * {
  border-block-start: 1px solid var(--hairline);
}
.ctms-export-answers tbody tr.ctms-export-answer:first-child > * {
  border-block-start: none;
}
.ctms-export-answers th[scope="row"] {
  font-weight: 400;
  color: var(--text-muted);
  width: 38%;
}
.ctms-export-entry > th {
  padding-block-start: 1rem;
  font-weight: 600;
  color: var(--text);
}
.ctms-export-child > td {
  padding-inline-start: 1rem;
}
/* An answer's history: indented under the answer it belongs to, quiet enough
   that the record still reads as a record rather than as a log (§9). */
.ctms-export-history > td {
  padding-block-start: 0;
  padding-inline-start: 1rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-export-history ul {
  margin: 0.25rem 0 0;
  padding-inline-start: 1rem;
}
.ctms-export-history li {
  margin-block-end: 0.25rem;
}
.ctms-export-history-title {
  font-weight: 500;
}
.ctms-export-change {
  font-weight: 500;
  color: var(--text);
}
.ctms-export-appendix {
  margin-block-start: 1rem;
  font-size: 0.8125rem;
}
.ctms-export-appendix > h3 {
  font-size: 0.8125rem;
  font-weight: 600;
  margin: 0 0 0.25rem;
}
.ctms-export-appendix ul {
  margin: 0;
  padding-inline-start: 1rem;
}
.ctms-export-appendix li {
  margin-block-end: 0.25rem;
}
/* The strike is on the SPAN, never on a block that wraps the note saying who
   withdrew it: text-decoration inherits into descendants and cannot be switched
   off by them (the M7.5 signature fix, same trap). */
.ctms-export-signature {
  margin-block-end: 0.75rem;
}
.ctms-export-signature > p {
  margin: 0;
}

@media print {
  /* 1. Undo the shell's scroll model. Without this #main-content's own
        overflow-y:auto clips the document to one screenful and only the first
        page ever reaches the printer. */
  body.ctms-shell:has(.ctms-export-sheet) {
    height: auto;
    overflow: visible;
    padding-top: 0;
  }
  body:has(.ctms-export-sheet) #main-content {
    margin-inline-start: 0;
    height: auto;
    overflow: visible;
    padding: 0;
  }
  /* 2. The chrome is not part of the document. display:none rather than
        visibility:hidden, which would keep every box reserving its space. */
  body:has(.ctms-export-sheet) .ctms-navbar,
  body:has(.ctms-export-sheet) #sidebar,
  body:has(.ctms-export-sheet) #breadcrumbs,
  body:has(.ctms-export-sheet) #toasts,
  body:has(.ctms-export-sheet) #dialog,
  body:has(.ctms-export-sheet) #nav-progress,
  body:has(.ctms-export-sheet) .ctms-footer,
  body:has(.ctms-export-sheet) .ctms-export-actions {
    display: none;
  }
  /* 3. Paper is white in both themes. One line, no hardcoded colour: the whole
        M3 palette is light-dark() driven by color-scheme (§3), so this flips
        the sheet to ink-on-white without touching a single theme rule. */
  body:has(.ctms-export-sheet),
  body.dark:has(.ctms-export-sheet),
  body.light:has(.ctms-export-sheet) {
    color-scheme: light;
    background: #fff;
  }
  .ctms-export-sheet {
    max-width: none;
    margin-block-start: 0;
  }
  /* 4. Pagination. A long form FLOWS across sheets — forcing it onto one would
        drop content — but a row, an entry and a signature are units, and a
        heading must not be orphaned at the foot of a page. */
  .ctms-export-answers thead {
    display: table-header-group;
  }
  .ctms-export-answers tr,
  .ctms-export-signature,
  .ctms-export-appendix li {
    break-inside: avoid;
  }
  .ctms-export-form > .ctms-schedule-head,
  .ctms-export-appendix > h3 {
    break-after: avoid;
  }
  .ctms-export-form {
    break-inside: auto;
  }
  /* 5. Status chips keep their tint; their LABEL already carries the meaning
        (§3.1), so a monochrome printer loses nothing either way. */
  .ctms-export-sheet .chip {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
}

@page {
  margin: 15mm 12mm;
}

/* THE INVOICE'S MONEY BLOCK (M9.4).
   Decision 2's shape rendered: the sponsor's side, then the site's, each closed
   by a ruled total. Two totals, deliberately — what somebody owes and what
   arrives here answer different questions, and neither is the other's rounding.
   Tabular figures so the column of amounts lines up on the printed sheet. */
.ctms-invoice-actions {
  gap: 0.75rem;
  flex-wrap: wrap;
}
.ctms-invoice-totals {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin: 0;
  margin-inline-start: auto;
  max-width: 32rem;
  font-size: 0.875rem;
}
.ctms-invoice-totals > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
}
.ctms-invoice-totals dt {
  color: var(--text-muted);
}
.ctms-invoice-totals dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The two figures a reader is looking for: ruled off above, and stated in the
   surface's own weight rather than in a color (§3.1 — never color alone). */
.ctms-invoice-totals .ctms-invoice-total {
  border-block-start: 1px solid var(--line);
  padding-block-start: 0.375rem;
  margin-block-start: 0.25rem;
  font-size: 1rem;
  font-weight: 600;
}
.ctms-invoice-totals .ctms-invoice-total dt {
  color: var(--text);
  font-weight: 600;
}

@media print {
  /* The money block is the one thing on the sheet that must never split across
     two pages: half a total is a document nobody can check. */
  .ctms-invoice-totals {
    break-inside: avoid;
  }
}


/* ---------- 18. Monitor queries (M7.8) ---------- */

/* One thread rendering serves every face (queries.templ): flat and hairline-
   separated — a conversation ABOUT the record, never louder than the record
   itself. The accent bar is an outline hairline, not a status color: the chip
   in the head already says the state with a label (§3.1). */
.ctms-query-thread {
  margin-block: 0.75rem;
  padding: 0.5rem 0 0.5rem 1rem;
  border-inline-start: 3px solid var(--line);
  font-size: 0.8125rem;
}
.ctms-query-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.ctms-query-head > i {
  font-size: 1.25rem;
  color: var(--main);
}
.ctms-query-comments {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
}
.ctms-query-comments li {
  margin-block-end: 0.5rem;
}
.ctms-query-comments li > p {
  margin: 0.125rem 0 0;
  white-space: pre-line;
}
/* The site's voice reads as the other side of the conversation. */
.ctms-query-response {
  padding-inline-start: 0.75rem;
  border-inline-start: 2px solid var(--main);
}
.ctms-query-thread .field {
  margin-block-start: 0.75rem;
}
/* The submit under the answer box needs its own gap: BeerCSS's sibling-margin
   rule (`:not(progress.max)+:is(address,…,.field,…)`) lists `.field` but not
   `button`, so a button that FOLLOWS a field inherits nothing and lands flush
   on the textarea's border — the site's inline respond, the monitor's
   follow-up comment and the open-a-query form all end that way. */
.ctms-query-thread > button,
.ctms-query-open > form > button,
.ctms-query-act > form > button {
  margin-block-start: 0.75rem;
}
/* The two disclosures (open a query, add a comment): link-postured summaries,
   with the pointer the pre-flight checklist demands on every clickable —
   summaries get none by default. */
.ctms-query-open > summary,
.ctms-query-act > summary {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: var(--main);
  font-size: 0.8125rem;
  list-style: none;
}
.ctms-query-open > summary::-webkit-details-marker,
.ctms-query-act > summary::-webkit-details-marker {
  display: none;
}
.ctms-query-open > summary > i,
.ctms-query-act > summary > i {
  font-size: 1.125rem;
}
.ctms-query-open,
.ctms-query-act {
  margin-block: 0.5rem;
}
.ctms-query-open > form,
.ctms-query-act > form {
  max-width: 32rem;
}
.ctms-query-act-row {
  display: flex;
  gap: 0.5rem;
  margin-block-start: 0.5rem;
}
.ctms-query-appendix {
  margin-block-start: 1.5rem;
}
/* Threads inside the monitor pane's answer table sit under their field's own
   row; the cell must not double-pad what the thread already spaces. */
.ctms-export-queries > td {
  padding-block-start: 0;
}

/* ---------- 18b. Lab stock (M8.2) ---------- */

/* An alerts cell can hold three chips; they wrap rather than widening the
   column past the count beside them. */
.ctms-chip-row {
  display: flex;
  gap: 0.375rem;
  flex-wrap: wrap;
}

/* The scan screen's running log: a flat list, hairline-separated, that a
   coordinator glances at without leaving the input. No card — the page's own
   background carries it (§3.0.5). */
.ctms-scan-log {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  font-size: 0.8125rem;
}
.ctms-scan-log li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding-block: 0.5rem;
  border-block-end: 1px solid var(--line);
}
.ctms-scan-log li:last-child {
  border-block-end: none;
}

/* The contract item's trigger block (M9.2) is a bare wrapper whose only job is
   to be an HTMX swap target — but BeerCSS turns a CLASS-LESS div holding a
   direct-child <input> into an inline-flex field wrapper:

     :is(div:not([class]), div[class="active"]):has(> :not(.responsive) ~ :is(menu, .tooltip, input))

   and the hidden visit_id input inside it is exactly that shape, so the two
   selects would sit side by side and collide. The CLASS is what defeats the
   selector; the declaration below states the intent out loud so nobody deletes
   an apparently unused class. */
.ctms-item-trigger {
  display: block;
}

/* ---------- 18c. Charts & stat tiles (M10.1, §4.9) ---------- */

/* A chart is a flat figure on the page background like every table (§4.2):
   no card, no border, no shadow. It is CAPPED rather than full-bleed: a chart
   is a reading-heavy view (§2.4), and across a 1500px laptop the value at a
   bar's tip ends up an inch of white away from the label it belongs to. */
.ctms-chart {
  margin: 0;
  max-width: 60rem;
}
.ctms-chart figcaption {
  margin-block-start: 0.75rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.ctms-chart-rows {
  display: flex;
  flex-direction: column;
  /* The gap IS the separation between adjacent bars — never a stroke around
     them (dataviz: the surface gap is the mechanism). */
  gap: 0.5rem;
}

/* label | bar | value. The label column is capped rather than fixed so a long
   recruitment status name wraps instead of squeezing the bar to nothing. */
.ctms-chart-row {
  display: grid;
  grid-template-columns: minmax(6rem, 14rem) 1fr auto;
  align-items: center;
  gap: 0.75rem;
}
.ctms-chart-label {
  font-size: 0.8125rem;
  color: var(--text);
  overflow-wrap: anywhere;
}
.ctms-chart-note {
  margin-inline-start: 0.375rem;
  font-size: 0.6875rem;
  color: var(--text-muted);
}
/* The bar is an ordinary block sized by CSS; its rect widths are percentages
   OF THIS ELEMENT, so nothing scales and the 4px radius stays 4px. */
.ctms-chart-bar {
  display: block;
  width: 100%;
  height: 14px;
  overflow: visible;
}
.ctms-chart-track {
  fill: var(--chart-track);
}
.ctms-chart-fill.tone-series {
  fill: var(--chart-series);
}
.ctms-chart-fill.tone-muted {
  fill: var(--chart-muted);
}
.ctms-chart-fill.tone-terminal {
  fill: var(--chart-terminal);
}

/* THE RAMP, APPLIED (M11.5 → M13.6). Seven element families wear the same five
   identity slots; all that differs between them is WHICH property paints — fill
   for the SVG marks, background for the legend swatch, stroke for the line. So
   the ramp is stated once here, by property, rather than restated per family.
   A mark carries EITHER a slot class or a tone class, never both
   (components.rampClasses / Point.toneClass). */
:is(.ctms-chart-slice, .ctms-chart-composition-seg, .ctms-chart-fill,
    .ctms-chart-stack-seg, .ctms-chart-dot).slot-1 { fill: var(--chart-cat-1); }
:is(.ctms-chart-slice, .ctms-chart-composition-seg, .ctms-chart-fill,
    .ctms-chart-stack-seg, .ctms-chart-dot).slot-2 { fill: var(--chart-cat-2); }
:is(.ctms-chart-slice, .ctms-chart-composition-seg, .ctms-chart-fill,
    .ctms-chart-stack-seg, .ctms-chart-dot).slot-3 { fill: var(--chart-cat-3); }
:is(.ctms-chart-slice, .ctms-chart-composition-seg, .ctms-chart-fill,
    .ctms-chart-stack-seg, .ctms-chart-dot).slot-4 { fill: var(--chart-cat-4); }
:is(.ctms-chart-slice, .ctms-chart-composition-seg, .ctms-chart-fill,
    .ctms-chart-stack-seg, .ctms-chart-dot).slot-5 { fill: var(--chart-cat-5); }
.ctms-chart-swatch.slot-1 { background: var(--chart-cat-1); }
.ctms-chart-swatch.slot-2 { background: var(--chart-cat-2); }
.ctms-chart-swatch.slot-3 { background: var(--chart-cat-3); }
.ctms-chart-swatch.slot-4 { background: var(--chart-cat-4); }
.ctms-chart-swatch.slot-5 { background: var(--chart-cat-5); }
.ctms-chart-stroke.slot-1 { stroke: var(--chart-cat-1); }
.ctms-chart-stroke.slot-2 { stroke: var(--chart-cat-2); }
.ctms-chart-stroke.slot-3 { stroke: var(--chart-cat-3); }
.ctms-chart-stroke.slot-4 { stroke: var(--chart-cat-4); }
.ctms-chart-stroke.slot-5 { stroke: var(--chart-cat-5); }
/* Values wear text tokens, never the mark's colour: identity comes from the
   coloured bar beside them (dataviz). */
.ctms-chart-value {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text);
  white-space: nowrap;
}
.ctms-chart-share {
  margin-inline-start: 0.375rem;
  font-weight: 400;
  color: var(--text-muted);
}

/* ---------- The line (M11.5) ----------
   Percent x/y on <line> and <circle> rather than a stretched viewBox, so the
   stroke is 2px at every container width and the dots stay round. */
.ctms-chart-line {
  display: block;
  width: 100%;
  height: 180px;
  overflow: visible;
}
/* Recessive, never dashed: the baseline says where zero is and nothing else. */
.ctms-chart-baseline {
  stroke: var(--chart-track);
  stroke-width: 1;
}
.ctms-chart-stroke {
  stroke: var(--chart-series);
  stroke-width: 2;
  stroke-linecap: round;
}
.ctms-chart-dot {
  fill: var(--chart-series);
}
/* One cell per period, so the labels sit under the vertices they name. Only a
   selection of them carries text (the component decides); the empty cells are
   what keeps the rest aligned. */
.ctms-chart-line-labels {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  margin-block-start: 0.5rem;
}
.ctms-chart-line-label {
  font-size: 0.6875rem;
  color: var(--text-muted);
  text-align: center;
  overflow-wrap: anywhere;
}

/* ---------- The pie (M11.5) ----------
   The one viewBox in the system, and it earns it: a circle must stay a circle,
   and there is no text inside it to be scaled by one. */
.ctms-chart-pie-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2rem;
}
.ctms-chart-pie {
  flex: 0 0 auto;
  width: 176px;
  height: 176px;
}
/* Adjacent wedges are separated by a GAP IN THE SURFACE COLOUR, never by a
   stroke of their own (dataviz: the surface gap is the mechanism). */
.ctms-chart-slice {
  stroke: var(--page);
  stroke-width: 1.5;
}
.ctms-chart-slice.tone-muted { fill: var(--chart-muted); }

/* The legend is the identity channel, so it is a real list: swatch, name,
   figure — the same three things the table repeats underneath. */
.ctms-chart-legend {
  /* Capped rather than free to fill the figure, for the same reason the figure
     itself is (§2.4): across a wide laptop a legend that grows puts each
     wedge's figure an inch of white away from the name it belongs to. */
  flex: 1 1 16rem;
  max-width: 30rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ctms-chart-legend li {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 0.625rem;
}
.ctms-chart-swatch {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 3px;
  align-self: center;
}
.ctms-chart-swatch.tone-muted { background: var(--chart-muted); }
/* A bucket with no wedge keeps its place in the reading order and shows no
   colour, because there is nothing on the picture to match it to. */
.ctms-chart-swatch.is-empty {
  background: none;
}

/* ---------- The column (M11.8) ----------
   A bar turned, and styled as one: percent y/height on its <rect>s against its
   own box, no viewBox, so the 4px radius stays 4px and the labels under it keep
   the type scale at every container width. */
.ctms-chart-columns {
  display: grid;
  grid-auto-flow: column;
  /* Equal tracks, so every mark has the same width and only its HEIGHT is data
     — a column widened by its own label would read as a second magnitude. The
     floor is what lets many buckets scroll rather than collapse to hairlines;
     with few, 1fr still fills the figure. */
  grid-auto-columns: minmax(2.5rem, 1fr);
  align-items: end;
  /* The gap IS the separation between adjacent columns — never a stroke, the
     bar's own mechanism. */
  gap: 0.5rem;
  overflow-x: auto;
}
.ctms-chart-column {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.375rem;
  min-width: 0;
}
.ctms-chart-column-mark {
  display: block;
  width: 100%;
  height: 160px;
}
/* The figure sits over a column instead of beside a bar, so it may wrap: a
   nowrap number in a 2.5rem track is a number outside its own column. */
.ctms-chart-column .ctms-chart-value {
  font-size: 0.75rem;
  white-space: normal;
  text-align: center;
}
.ctms-chart-column .ctms-chart-share {
  display: block;
  margin-inline-start: 0;
}
.ctms-chart-column-label {
  font-size: 0.75rem;
  color: var(--text);
  text-align: center;
  overflow-wrap: anywhere;
  min-width: 0;
}

/* ---------- The composition bar (M11.8) ----------
   The pie's picture in a straight line, so it wears the pie's colours: the same
   validated ramp, the same legend, the same grey past the fifth identity. */
.ctms-chart-composition {
  display: block;
  width: 100%;
  height: 28px;
  /* The ends are rounded HERE rather than on the first and last segments,
     because "this shape has round ends" is true of every composition bar and is
     therefore not data (§6) — a radius on the segments would move with the data
     and round the wrong end the moment the leading bucket held nobody. */
  clip-path: inset(0 round 4px);
}
.ctms-chart-composition-seg.tone-muted { fill: var(--chart-muted); }
/* Adjacent parts are separated by the SURFACE showing through, never by a
   stroke of their own — the wedge rule, drawn as a rect because this shape has
   no stroke to give. */
.ctms-chart-composition-gap {
  fill: var(--page);
}
.ctms-chart-composition-figure .ctms-chart-legend {
  margin-block-start: 1rem;
}

/* ---------- The pictures made of PARTS (M11.9, grown by M11.10 and M11.11) ----
   A second dimension is a set of identities, so all six of these — the grouped
   pair, the stacked pair, the stacked-to-100 pair and the multi-line — wear the
   same validated ramp the pie and the composition bar wear, assigned by POSITION
   IN THE SERIES LIST rather than in drawing order, so a category absent from one
   bucket does not shift the colours of the ones after it.

   THE LAYOUT IS ALWAYS THE PLAIN PICTURE'S. Every rule below is a fill, a stroke
   or a two-pixel gap; nothing here re-decides how wide a bucket is or how tall a
   row, because those belong to the bar and column families above and a second
   set of them is where the two would come to disagree. */

/* The grouped bar: one bucket's bars stacked in the row the bar chart gives one
   bar. The GAP is the separation, never a stroke — the bar's own mechanism. */
.ctms-chart-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
/* Thinner than a lone bar, because five of them share one row's height. The
   svg's rect heights are pixels of this same number (the component's own
   constant), so the two cannot drift. */
.ctms-chart-row-grouped .ctms-chart-bar {
  height: 10px;
}
/* The label sits at the TOP of its group rather than centred on it: a name
   floating beside the third of five bars reads as belonging to that bar. */
.ctms-chart-row-grouped {
  align-items: start;
}

/* The stacked column: the column's own box, cut. It reuses .ctms-chart-columns
   and .ctms-chart-column-mark wholesale — the picture is the column, and a
   second set of layout rules would be where the two came to disagree about how
   wide a bucket is. */
.ctms-chart-stack-seg.tone-muted { fill: var(--chart-muted); }

/* The GROUPED COLUMN (M11.10) is the grouped bar turned, so its marks are
   separated the same way — a flex GAP rather than x-offsets computed per mark,
   because two pixels of separation is not data (§6). The row direction is the
   only difference between this rule and .ctms-chart-group above it. */
.ctms-chart-column-group {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  gap: 2px;
  width: 100%;
  min-width: 0;
}
/* Each column of a group shares the cell equally: only the HEIGHT is data, so a
   column widened by how many series there are would read as a second magnitude. */
.ctms-chart-column-group .ctms-chart-column-mark {
  flex: 1 1 0;
  min-width: 0;
}

/* The MULTI-LINE (M11.10) wears the ramp as a STROKE, which is the first time
   in this stylesheet it is anything but a fill. No new tokens: these are plain
   colours and .ctms-chart-baseline already proves they work in stroke position.
   The dot takes the same identity as the line it sits on, or the two would
   disagree about which series a vertex belongs to. */
.ctms-chart-stroke.tone-muted { stroke: var(--chart-muted); }
.ctms-chart-dot.tone-muted { fill: var(--chart-muted); }
/* A SURFACE-COLOURED RING, so vertices that nearly coincide still read as two.
   It is the same device adjacent wedges and stacked segments already use — the
   gap is the surface showing through — asked of an overlap instead of a
   neighbour, and it is scoped to the identity classes so the single line's dots
   are unchanged.

   IT DOES NOT RESCUE AN EXACT TIE, and cannot: two series holding the same
   figure in the same period land on the same pixel, and the last one drawn
   covers the others whatever ring it wears. That is not a picture lying — they
   really are at the same point — and the legend names every series while the
   table carries every figure, which is where a reader tells them apart (§7). */
.ctms-chart-dot[class*="slot-"],
.ctms-chart-dot.tone-muted {
  stroke: var(--page);
  stroke-width: 1.5;
}

/* One legend under any of these pictures, in the composition bar's own spacing:
   the colours are the identity channel and the names have to sit near them. The
   multi-line's legend follows its LABEL ROW rather than its svg, which is why
   that selector is here and not folded into the two above it. */
.ctms-chart-rows + .ctms-chart-legend,
.ctms-chart-columns + .ctms-chart-legend,
.ctms-chart-line-labels + .ctms-chart-legend {
  margin-block-start: 1rem;
}

/* Stat tiles: the form a number takes when it IS the answer. Flat, on the page
   background, separated by space alone. */
.ctms-stat-row {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  margin-block-end: 1.5rem;
}
.ctms-stat {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}
.ctms-stat-label {
  font-size: 0.75rem;
  color: var(--text-muted);
}
/* Proportional figures deliberately: tabular-nums gives every digit a zero's
   width, which reads loose at display sizes (dataviz). Columns of numbers keep
   tabular-nums; a headline figure does not. */
.ctms-stat-value {
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--text);
}
.ctms-stat-value.accent {
  color: var(--chart-terminal);
}
.ctms-stat-unit {
  margin-inline-start: 0.125rem;
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--text-muted);
}

@media (max-width: 767px) {
  /* Below the tablet breakpoint the label gets its own line: a 6rem label
     column beside a bar leaves neither of them readable. */
  .ctms-chart-row {
    grid-template-columns: 1fr auto;
  }
  .ctms-chart-label {
    grid-column: 1 / -1;
  }
  /* The legend goes UNDER the pie rather than beside it, and its own rows keep
     their three columns: a 16rem legend beside a 176px circle at 360px wide
     leaves the names one word per line. */
  .ctms-chart-pie-row {
    gap: 1.25rem;
  }
  .ctms-chart-legend {
    flex-basis: 100%;
  }
  /* The line keeps every vertex and every one of the labels the component
     selected; they just get less room, and the table under it is where the
     periods are read one by one anyway (§7). */
  .ctms-chart-line-label {
    font-size: 0.625rem;
  }
  /* Columns keep every bucket and scroll instead of collapsing: the marks are
     the picture, and a 2rem track still reads as a height. Their labels take
     the line's treatment for the line's reason — less room, and the table under
     the chart is where a bucket is read one by one anyway (§7). */
  .ctms-chart-columns {
    grid-auto-columns: minmax(2rem, 1fr);
  }
  .ctms-chart-column-label,
  .ctms-chart-column .ctms-chart-value {
    font-size: 0.625rem;
  }
  .ctms-stat-row {
    gap: 1.25rem;
  }
}

/* ---------- 18d. Insights report frame (M10.1, kept at M13.1) ---------- */

/* Phase 13 deleted the aggregate engine these were written for and kept the
   frame, because an audience index of cards is the same navigation whatever a
   report turns out to be. The swapped-results rule went with the engine: the
   region it dimmed does not exist until the table does (M13.2), and a rule
   nothing wears is a rule nobody can check. */

/* The audience index: one card per report the user may both reach and source.
   Flat, on the page background, separated by hairlines and space (§3.0.5) —
   these are navigation targets, not elevated surfaces. */
.ctms-report-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* A capped track rather than 1fr: a card that stretches to a 1500px screen
     is a banner, and one that shrinks to 17rem turns its description into a
     six-line column. The `min()` is what keeps the floor from becoming an
     overflow: below 20rem of content box there is no width left to hold the
     minimum, and a track that insists on it spills the card past the page
     instead (§8, phone widths). */
  grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 22rem));
  gap: 1rem;
}
.ctms-report-card {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  column-gap: 0.75rem;
  row-gap: 0.25rem;
  height: 100%;
  padding: 1rem;
  border-radius: 12px;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  background: var(--card);
  transition: background 150ms ease;
}
.ctms-report-card:hover,
.ctms-report-card:focus-visible {
  background: var(--hover);
}
.ctms-report-card > i {
  grid-row: 1 / span 2;
  align-self: start;
  font-family: var(--font-icon);
  font-size: 1.5rem;
  color: var(--main);
}
.ctms-report-card-title {
  font-weight: 600;
}
.ctms-report-card-text {
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* ---------- 18e. The report table and its builder (M13.2) ---------- */

/* "Rows are: participations" — the first thing a reader is told about what they
   are counting, so it sits under the title as a line of its own rather than as a
   caption on the table. Flat, on the page background: it is a statement about
   the report, not a control (§3.0.5). */
.ctms-report-grain {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin: 0.25rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}
.ctms-report-grain > i {
  font-family: var(--font-icon);
  font-size: 1.125rem;
}

/* A header cell's second line: what a column is ABOUT when that differs from
   what a row is ("per participant", M13.3). Its own line so the header still
   reads as one word-or-two, and lighter than the label because it qualifies the
   label rather than competing with it. */
.ctms-column-note {
  display: block;
  font-weight: 400;
  font-size: 0.6875rem;
  color: var(--text-muted);
}

/* A note under a table or beside a control — the truncation line, "no columns
   yet". Secondary, because it explains the thing above it rather than being the
   thing. */
.ctms-report-note {
  margin: 0.5rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* The export link under the table (M13.5). A <nav> because BeerCSS styles a bare
   one as a flex ROW, which is what a row of table actions is — and it is inside
   the swapped region, so the file it points at is always the table above it. */
.ctms-report-actions {
  margin-top: 0.75rem;
}

/* THE COLUMN LIST is ordered and its order is the point, so it is an <ol> that
   keeps its numbers: they are what the arrows move, and a list of names with no
   positions would make "move up" a promise about nothing visible. */
.ctms-column-list {
  margin: 0;
  padding-inline-start: 1.5rem;
  display: grid;
  gap: 0.25rem;
  /* Stated rather than inherited: BeerCSS strips markers from every list, and
     these numbers are what the arrows move — "move left" is a promise about a
     position, so the position is on the screen. */
  list-style: decimal;
}
.ctms-column-list > li {
  /* Hairline separation, no card chrome (§3.0.5). The last row keeps its rule
     because the picker sits below it and the two are different things. */
  border-block-end: 0.0625rem solid var(--hairline);
}
/* The row lives INSIDE the item: `display: flex` on the <li> itself would
   replace its list-item display type and take the marker with it. */
.ctms-column-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.25rem;
}
.ctms-column-name {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The picker: a field and the button that acts on it, on one line where there is
   room and stacked where there is not. align-items:end keeps the button on the
   control's baseline rather than on its label's. */
.ctms-picker-row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.75rem;
}
.ctms-picker-row > .field {
  flex: 1 1 18rem;
  margin-block-end: 0;
}

/* THE READER'S FILTER BAR (M13.4). Flat like the participant registry's own
   advanced panel (§3.0.5): it separates from the table by spacing and by a
   single hairline above its actions, never by a card, a fill or a shadow. */
.ctms-report-filters {
  margin: 0;
}
/* A fieldset because that is what a labelled group of controls is; its border
   would be the app's only boxed form region. */
.ctms-filter-fields {
  border: none;
  margin: 0;
  padding: 0;
}
.ctms-filter-fields > legend {
  padding: 0;
  margin-block-end: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
/* One column's operator and its value read as ONE control, so they sit in one
   stack with the tighter of the app's two gaps between them. */
.ctms-filter-control {
  display: grid;
  gap: 0.25rem;
}
/* BeerCSS gives every .field after the first inside a non-.grid parent a top
   margin, through a compound :not()/:is() selector that outweighs a single
   class — the .ctms-criteria-picker-controls precedent, same override. */
.ctms-filter-control > .field,
.ctms-filter-pair > .field {
  margin-block: 0 !important;
}
/* The two ends of a range share a row and split it evenly, so "from" and "to"
   read as one span rather than as two unrelated numbers. */
.ctms-filter-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}
/* Grid items refuse to shrink below their content by default, and a date input
   carries a format hint wider than most cells: without this the pair pushes its
   column and the page scrolls sideways on a tablet (§8). */
.ctms-filter-pair > .field {
  min-width: 0;
}
/* The label a control with a single-operator vocabulary carries instead of a
   select: the same size and colour as a field's own floated label, so a row of
   controls lines up whether or not each one has an operator to choose. */
.ctms-filter-label {
  margin: 0 0 0.25rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}
/* WHAT A CONDITION LEAVES OUT (M13.7), under the control that states it and
   beside the chip that carries it. It is a fact about the surviving rows rather
   than an error or a helper, so it reads in the same secondary voice as the
   control's own label — the .ctms-filter-label size and colour, one step of the
   4px grid below whatever it qualifies. */
.ctms-filter-note {
  display: block;
  margin: 0.25rem 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}
/* A VOCABULARY'S OPTIONS WRAP HERE, where everywhere else they stack. The
   registry's own checkbox groups hold three or four options and a column reads
   best; a participation status holds TEN, and stacking those made one control
   four times the height of the row it shares, leaving the two beside it floating
   in whitespace. Found in a browser, not in review. */
.ctms-report-filters .ctms-checkbox-group-options {
  flex-direction: row;
  flex-wrap: wrap;
  column-gap: 0.75rem;
}
/* A BAKED condition is locked, and the padlock says so beside the words — colour
   alone is never the signal (§3.1). It is smaller than the text it qualifies. */
.ctms-filter-lock {
  font-size: 1rem;
  vertical-align: middle;
  color: var(--text-muted);
}
/* The builder's own list of baked conditions is unordered where the column list
   is numbered: the predicates are ANDed, so their order says nothing a reader
   could see, and printing positions would invite an author to reorder something
   that has no order. */
.ctms-filter-list {
  padding-inline-start: 0;
  list-style: none;
}
.ctms-filter-list .ctms-column-name {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}

/* ---------- 18f. A report's charts (M13.6) ---------- */

/* The pictures above the table. A GRID rather than a stack, because a report
   often carries two or three small charts and a column of them pushes the thing
   the report IS off the screen — and a capped track rather than 1fr, the report
   cards' own rule: a bar chart stretched across 1500px reads as a banner. */
.ctms-chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  gap: 1.5rem 2rem;
}

/* One chart. FLAT (§3.0.5): no card, no border — the heading and the space
   around it are what separate it from the next one. It is also the swap target,
   so it must keep its box whether it holds a placeholder or a picture. */
.ctms-chart-block {
  min-width: 0;
}
.ctms-chart-title {
  margin: 0 0 0.5rem;
  font-size: 0.9375rem;
  font-weight: 600;
}

/* The lazy placeholder, in flight, is now components.Loader — the shared
   .ctms-loader-block at §16b, which inherited this rule's shape and the reason
   for it (a row, not a centred block, so the arriving chart does not make the
   page jump by the height of a spinner sitting in the middle of nothing). */

/* The picture's text alternative (§7). Folded away rather than absent: it is in
   the DOM and reachable, and a report with four charts does not push its own
   table off the screen. */
.ctms-chart-data {
  margin-top: 0.5rem;
}
.ctms-chart-data > summary {
  cursor: pointer;
  font-size: 0.8125rem;
  color: var(--text-muted);
}
.ctms-chart-table {
  width: 100%;
  margin-top: 0.5rem;
  border-collapse: collapse;
  font-size: 0.8125rem;
}
.ctms-chart-table th,
.ctms-chart-table td {
  padding: 0.25rem 0.5rem 0.25rem 0;
  text-align: start;
  font-weight: 400;
  border-block-end: 0.0625rem solid var(--hairline);
}
.ctms-chart-table thead th {
  font-weight: 600;
  color: var(--text-muted);
}
/* THE BUCKET'S OWN ROW is the whole and its parts are under it, so it carries
   the weight — it is the one figure that cannot be recovered by adding the
   others up when the measure is an average. */
.ctms-chart-table .ctms-chart-whole > th,
.ctms-chart-table .ctms-chart-whole > td {
  font-weight: 600;
}

/* The chart builder's controls. Two columns where there is room: a chart is
   eight small selects, and a single column of them is a form nobody can see the
   shape of. */
.ctms-chart-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: 0 1rem;
  align-items: start;
}

/* ---------- 19. Reduced motion (§3.5) ---------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition: none !important;
    animation: none !important;
  }
}
