@import url("./fonts/cairo.css");

/* ==========================================================================
   ACE Stellenbosch — Talent ID application
   Tokens carried over verbatim from proposal/assets/ace.css so the demo and the
   pitch read as one product. Cairo is the brand face; the gold bar and the
   near-square 2px radius are lifted from ACE's own recruitment poster.

   Mobile-first throughout: Rose's brief is explicit that "the majority of
   applications are done on cell phones", and the previous build's breakpoints
   were written but never actually verified below 800px.
   ========================================================================== */

:root {
  --ink:#000; --paper:#fff; --bone:#f8f9fa; --slate:#212529;
  --gold:#ffb612; --gold-ink:#b37a00; --green:#007a4d;
  --blue:#007fc0; --blue-deep:#0a6ea8; --red:#de3831;
  --n-100:#f1f3f5; --n-200:#e4e7ea; --n-300:#ced4da; --n-500:#868e96; --n-700:#495057;
  --face:"Cairo", system-ui, -apple-system, "Segoe UI", sans-serif;
  --bar:3px; --r:2px;
  /* The black top bar and its white text stay black and white in BOTH themes,
     so they are their own tokens rather than --ink/--paper, which swap. */
  --chrome:#000; --on-chrome:#fff; --on-chrome-dim:#ced4da;
  /* Tinted grounds that carry ordinary (--ink) text. Light values are the
     hex these rules used before the dark theme (2026-10-06). */
  --tint-red:#fdecea; --tint-red-soft:#fffafa; --tint-amber:#fff8e6;
  --tint-amber-2:#fff6e0; --tint-green:#eaf7f0; --tint-gold:#fffaf0;
  --red-ink:#b1261f;
  color-scheme:light;
}

/* ── dark theme (Paul, 2026-10-06) ─────────────────────────────────────────
   Chosen with the Light / Dark buttons at the top of every page
   (_theme_toggle.html) and remembered in that browser. Light is the default,
   whatever the device prefers. Only tokens change here: a rule that needs a
   colour that differs by theme takes it from a token, never a hex. */
:root[data-theme="dark"] {
  --ink:#e9ecef; --paper:#1d2125; --bone:#131518;
  --gold-ink:#f2b84b; --red:#ef5a50; --blue:#4db0e6;
  --n-100:#262a2f; --n-200:#30353b; --n-300:#4a5259; --n-500:#9aa3ab; --n-700:#c3cad1;
  --tint-red:#3b211f; --tint-red-soft:#2a1d1d; --tint-amber:#352c16;
  --tint-amber-2:#3a2f14; --tint-green:#183025; --tint-gold:#33291a;
  --red-ink:#ff8a80;
  color-scheme:dark;
}

/* The two buttons. In the top bar on signed-in pages; on a public page, a
   slim strip above everything else. type="button", so inside a form they
   never send it. */
.theme-toggle { display:inline-flex; gap:0; }
.theme-toggle button {
  font-family:var(--face); font-size:.74rem; font-weight:700; cursor:pointer;
  padding:.22rem .55rem; border:1px solid var(--n-300); background:transparent;
  color:inherit; line-height:1.3;
}
.theme-toggle button:first-child { border-radius:var(--r) 0 0 var(--r); }
.theme-toggle button:last-child { border-radius:0 var(--r) var(--r) 0; border-left:0; }
.theme-toggle button[aria-pressed="true"] { background:var(--gold); color:#000; border-color:var(--gold); }
.topbar .theme-toggle button { border-color:rgba(255,255,255,.3); }
.theme-strip { display:flex; justify-content:flex-end; padding:.35rem .9rem 0;
  color:var(--ink); }
/* ACE's wordmark is black artwork. On a dark ground it is drawn white, as
   the top bar always draws it -- the login card is the case this is for. */
:root[data-theme="dark"] img[src$="ace-wordmark.png"] { filter:invert(1) brightness(2); }

*,*::before,*::after { box-sizing:border-box; }
html { -webkit-text-size-adjust:100%; }
body {
  margin:0; font-family:var(--face); font-size:16px; line-height:1.5;
  color:var(--ink); background:var(--bone);
  font-variant-numeric:tabular-nums;
}
a { color:inherit; }
h1,h2,h3 { margin:0 0 .4em; line-height:1.2; font-weight:700; }
h1 { font-size:1.5rem; } h2 { font-size:1.15rem; } h3 { font-size:1rem; }

.eyebrow {
  text-transform:uppercase; letter-spacing:.12em; font-size:.68rem;
  font-weight:700; color:var(--n-700);
}

/* ── shell ─────────────────────────────────────────────────────────────── */
/* ONE thing is pinned when a page scrolls (docs/responsive20261014.md Part 5,
   2026-10-14). On a tablet or PC it is the menu, nav.main, and the top bar
   scrolls away; on a phone it is the top bar's one slim row, which holds the
   Menu. Both used to be sticky at top:0, so from the first scroll the menu
   slid under the top bar at every width and was never usable mid-page. */
.topbar {
  background:var(--chrome); color:var(--on-chrome);
  border-bottom:var(--bar) solid var(--gold);
  position:relative; z-index:20;
}
.topbar-inner {
  display:flex; align-items:center; gap:.5rem .75rem; flex-wrap:wrap;
  padding:.6rem .9rem; max-width:1280px; margin:0 auto;
}
.topbar img { height:26px; width:auto; filter:invert(1) brightness(2); }
.topbar .sys { font-size:.72rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--gold); font-weight:700; }
.topbar .spacer { flex:1; }
.topbar a.home { display:flex; align-items:center; gap:.75rem; text-decoration:none; }
.topbar a.home:hover .sys, .topbar a.home:focus-visible .sys { text-decoration:underline; }
/* A persistent credit. The top bar heads every screen, so this is the one place the
   builder's name is visible on every screen without competing with ACE's own
   wordmark beside it -- hence grey rather than the brand bronze, and hence it
   is the only thing here that is not a control. */
.byline {
  display:flex; align-items:center; gap:.3rem;
  font-size:.66rem; color:var(--n-500); text-decoration:none;
  padding-left:.6rem; margin-left:.2rem;
  border-left:1px solid rgba(255,255,255,.18);
  white-space:nowrap;
}
.byline img { height:11px; width:auto; }
.byline:hover span, .byline:focus-visible span { color:var(--on-chrome); text-decoration:underline; }
/* On a narrow screen the controls matter more than the credit; the footer
   still carries it. */
@media (max-width:739.98px) { .byline { display:none; } }
.rolepill {
  background:rgba(255,255,255,.12); border:1px solid rgba(255,255,255,.25);
  color:var(--on-chrome); font-family:var(--face); font-size:.78rem;
  padding:.28rem .5rem; border-radius:var(--r);
  /* A select sizes itself to its longest option -- 170px here, which shoved
     "Sign out" off a 360px screen. min-width:0 lets flexbox shrink it. */
  min-width:0; max-width:100%; flex:0 1 auto; text-overflow:ellipsis;
}
/* The open dropdown is drawn by the browser, not by this stylesheet, so it
   lands on a white popup background while inheriting the white text set above
   for the dark top bar. Result: every option was invisible until the pointer
   highlighted it. Both colours have to be stated on the option itself --
   nothing in the select's own rules reaches the popup. */
.rolepill option {
  color: var(--ink);
  background-color: var(--paper);
}
.topbar a.out { color:var(--on-chrome-dim); font-size:.78rem; text-decoration:none; }
/* ⚙: your name opens your account; a login with no mobile number is asked for
   one (docs/20oct2026.md Part A) -- gold, the chrome's one accent. */
.topbar .whoami a { color:inherit; text-decoration:none; }
.topbar a.nag { color:var(--gold); font-size:.78rem; font-weight:700; text-decoration:none; }
/* Users (docs/20oct2026.md D4): ten columns beside the filter tree. An address
   or a number may break anywhere, and the buttons stack, so a tablet fits it. */
td .addr { overflow-wrap:anywhere; }
table.users-table td.acts form, table.users-table td.acts .btn { display:block; }
table.users-table td.acts .btn { margin:0 0 .3rem; }
@media (min-width:740px) {
  table.users-table td { font-size:.8rem; }
  table.users-table th, table.users-table td { padding-left:.25rem; padding-right:.25rem; }
}

nav.main {
  background:var(--slate); overflow-x:auto; -webkit-overflow-scrolling:touch;
  position:sticky; top:0; z-index:19;
}
nav.main ul {
  display:flex; gap:0; list-style:none; margin:0 auto; padding:0;
  max-width:1280px; min-width:max-content;
}
nav.main a {
  display:block; padding:.6rem .85rem; color:var(--on-chrome-dim);
  text-decoration:none; font-size:.82rem; font-weight:600; white-space:nowrap;
  border-bottom:3px solid transparent;
}
nav.main a[aria-current="page"] { color:var(--on-chrome); border-bottom-color:var(--gold); }
/* A group (Results, Data, Communication) and the row of its pages it opens
   underneath, as bubbles. The nav is --slate in both themes, so these colours
   are fixed rather than tokens that swap. */
nav.main a.navgroup[aria-current="true"] { color:var(--on-chrome); border-bottom-color:var(--gold); }
nav.main a.navgroup[aria-expanded="true"] { color:var(--on-chrome); }
nav.main .caret { font-size:.7em; margin-left:.3em; display:inline-block; transition:transform .15s; }
nav.main a.navgroup[aria-expanded="true"] .caret { transform:rotate(180deg); }
.subnav { background:#2b3036; border-top:1px solid rgba(255,255,255,.08); }
.subnav[hidden] { display:none; }
nav.main .subnav ul { gap:.4rem; padding:.45rem .9rem; flex-wrap:wrap; min-width:0; }
nav.main .subnav a {
  padding:.25rem .75rem; border-radius:999px; border:1px solid rgba(255,255,255,.18);
  background:rgba(255,255,255,.06); font-size:.78rem;
}
nav.main .subnav a:hover { color:var(--on-chrome); border-color:rgba(255,255,255,.4); }
nav.main .subnav a[aria-current="page"] { background:var(--gold); color:#000; border-color:var(--gold); }

/* ── the top bar on a phone (Part 5, 2026-10-14) ───────────────────────────
   Measured before: four rows, 201 px, and with the "Data / Viewing as" strip
   and the menu 46% of a 360 px screen before any content; the menu a 730 px
   strip with Data, Communication, Rules and Admin hidden off to the side.
   Now: one slim row -- the wordmark, Menu and Settings -- pinned; Menu opens
   every section as a grid of large buttons, each group's pages under it;
   Settings opens the controls. Both are <details>, so no script is needed.
   From 740 px up neither exists and the bar is as it was: below that the
   menu strip (730 px in English, 724 in Afrikaans) would scroll inside
   itself with Rules and Admin off the edge -- a phone held sideways is
   600-740 (review of 434914a). Tables become cards below 740 too. */
.topdrop > summary { list-style:none; cursor:pointer; }
.topdrop > summary::-webkit-details-marker { display:none; }
@media (min-width:740px) {
  .topdrop, .explain-more { display:none; }
  .settings-panel { display:contents; }
}
@media (max-width:739.98px) {
  .topbar { position:sticky; top:0; }
  .topbar-inner { padding:.35rem .55rem; gap:.4rem; flex-wrap:nowrap; }
  /* Measured at 360 in Afrikaans ("Kieslys", "Instellings"): every pixel of
     gap counts, so the spacer goes and Menu pushes itself right instead. */
  .topbar .spacer { display:none; }
  .phonemenu { margin-left:auto; }
  .topbar img { height:20px; }
  .topbar a.home { gap:.45rem; }
  .topbar .tour-btn { margin-right:0; padding:.3rem .4rem; }
  .tour-btn span { display:none; }
  nav.main { display:none; }

  .topdrop > summary {
    display:flex; align-items:center; gap:.25rem; white-space:nowrap;
    padding:.3rem .45rem; border:1px solid rgba(255,255,255,.3); border-radius:var(--r);
    font-size:.78rem; font-weight:700; color:var(--on-chrome);
  }
  .topdrop[open] > summary { background:var(--gold); color:var(--chrome); border-color:var(--gold); }
  .topdrop .ic { font-size:1rem; line-height:1; }

  /* The two panels open under the bar, over the page, and scroll on their
     own if the screen is short. */
  .menu-grid, .settings-panel {
    position:absolute; left:0; right:0; top:100%; z-index:21;
    max-height:calc(100dvh - 3.5rem); overflow-y:auto;
    padding:.7rem .7rem .9rem; border-bottom:var(--bar) solid var(--gold);
  }
  .menu-grid { background:var(--slate); }
  .menu-grid ul { list-style:none; margin:0; padding:0; }
  /* A group takes the full width, its pages in the same columns under its
     name; the single sections fill the cells around them (dense). */
  .menu-grid > ul, .menu-grid .mg-group ul { display:grid; gap:.45rem;
    grid-template-columns:repeat(auto-fill, minmax(min(100%, 9.5rem), 1fr)); }
  .menu-grid > ul { grid-auto-flow:row dense; }
  .menu-grid .mg-group { grid-column:1 / -1; }
  .menu-grid a {
    display:flex; align-items:center; min-height:44px; padding:.4rem .7rem;
    border:1px solid rgba(255,255,255,.18); border-radius:var(--r);
    background:rgba(255,255,255,.06); color:var(--on-chrome);
    font-size:.9rem; font-weight:600; text-decoration:none; overflow-wrap:anywhere;
  }
  .menu-grid a[aria-current="page"] { background:var(--gold); color:var(--chrome); border-color:var(--gold); }
  .mg-title { display:block; margin:.15rem 0 .3rem; font-size:.66rem; font-weight:700;
    letter-spacing:.12em; text-transform:uppercase; color:var(--gold); }

  /* Settings: the controls, one under another. Shown while Settings is open,
     where the browser has `:has`; without it they stay in the bar, wrapped,
     as they always were, and the Settings button goes. */
  .settings-panel { display:flex; flex-direction:column; align-items:stretch; gap:.7rem;
    background:var(--chrome); }
  .settings-panel .ctrl .rolepill { width:100%; font-size:.9rem; padding:.45rem .55rem; }
  .settings-panel .theme-toggle, .settings-panel .switch { align-self:flex-start; }
  .topbar .settings-panel .switch { margin-right:0; }
  .settings-panel a.out, .settings-panel a.nag { font-size:.9rem; padding:.3rem 0; }
}
@supports selector(:has(*)) {
  @media (max-width:739.98px) {
    .topbar:not(:has(details.settings[open])) .settings-panel { display:none; }
    .explain:not(:has(details.explain-more[open])) .explain-t,
    .explain:not(:has(details.explain-more[open])) .explain-role { display:none; }
    .explain-row { flex-wrap:nowrap; }
    .explain:has(details.explain-more[open]) .explain-row { flex-wrap:wrap; }
  }
}
@supports not selector(:has(*)) {
  details.settings, .explain-more { display:none; }
  @media (max-width:739.98px) {
    .topbar-inner { flex-wrap:wrap; }
    .settings-panel { position:static; display:contents; }
  }
}
.explain-more { margin-left:auto; }
.explain-more > summary { cursor:pointer; font-size:.7rem; font-weight:700; white-space:nowrap; }

main { max-width:1280px; margin:0 auto; padding:1rem .9rem 4rem; }

/* ── the three controls, and what they currently mean ──────────────────── */
/*
   Each control is a labelled group rather than a bare select. Three unlabelled
   dropdowns in a row is a puzzle: the first demo question was "what are these?"
   The label says what the control is; the strip below says what the current
   choice means, which is the part that actually needs explaining.
*/
.ctrl { display:flex; flex-direction:column; gap:.15rem; min-width:0; }
.ctrl-l {
  font-size:.58rem; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
  color:var(--on-chrome-dim); white-space:nowrap;
}

/* The dataset control is the consequential one, so it carries the outline. */
.datasetpill { border-color:var(--gold); font-weight:700; }

/* A disabled option still shows, greyed. The roles are real and enforced in
   the queries; they are simply not switchable in this demonstration. */
.rolepill option:disabled { color:var(--n-500); font-style:italic; }

.explain { font-size:.74rem; }
.explain-row {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:.15rem .5rem;
  padding:.4rem .9rem; max-width:1280px; margin:0 auto;
}
.explain-k {
  font-weight:700; letter-spacing:.1em; text-transform:uppercase;
  font-size:.6rem; opacity:.8; white-space:nowrap;
}
.explain-v { font-weight:700; white-space:nowrap; }
.explain-t { opacity:.9; min-width:0; }

/* One colour per dataset. Which records are on screen is the single most
   important thing to be unambiguous about -- illustrative data and a real
   child's record must never be mistaken for one another at a glance, and this
   row is visible on every page regardless of scroll.

   ACE's own data is BLUE, not red. Red reads as an error, and there is nothing
   wrong with ACE's records -- the bar's job is "know which data this is", not
   "something has gone wrong". --blue-deep rather than --blue because white on
   #007fc0 is 4.38:1, under the 4.5:1 the rest of this stylesheet holds to. */
.explain-sample  { background:var(--gold);      color:#000; }
.explain-real    { background:var(--blue-deep); color:var(--on-chrome); }
.explain-sandbox { background:var(--green);     color:var(--on-chrome); }
.explain-role    { background:var(--n-100); color:var(--n-700);
                   border-bottom:1px solid var(--n-200); }

.flashes {
  background:var(--n-100); border-bottom:1px solid var(--n-200);
  padding:.5rem .9rem;
}
.flashes p {
  max-width:1280px; margin:0 auto; font-size:.85rem; color:var(--n-700);
}

/* ── cards, tiles ──────────────────────────────────────────────────────── */

.panel {
  background:var(--paper); border:1px solid var(--n-200); border-radius:var(--r);
  padding:.9rem; margin-bottom:1rem; min-width:0;
}
.panel > h2 { display:flex; align-items:baseline; gap:.5rem; }
.panel .hint { font-size:.78rem; color:var(--n-700); font-weight:400; }

.tiles { display:grid; grid-template-columns:repeat(2,1fr); gap:.6rem; margin-bottom:1rem; }
.tiles.four { grid-template-columns:repeat(2,1fr); }
.tile {
  background:var(--paper); border:1px solid var(--n-200); border-radius:var(--r);
  padding:.7rem .8rem; border-top:var(--bar) solid var(--gold);
}
.tile .n { font-size:1.7rem; font-weight:700; line-height:1; }
.tile .l { font-size:.72rem; text-transform:uppercase; letter-spacing:.08em;
  color:var(--n-700); font-weight:700; margin-top:.3rem; }
.tile.bad { border-top-color:var(--red); }
.tile.good { border-top-color:var(--green); }

/* ── bars ──────────────────────────────────────────────────────────────── */

/* Grid children default to min-width:auto, which refuses to shrink below their
   min-content width. On a 360px screen that let a panel's content force its
   grid column to 538px and pushed the whole page sideways. Every grid child in
   this stylesheet therefore gets min-width:0 explicitly. */
.grid2 > *, .bars > *, .barrow > *, .tiles > * { min-width:0; }

.bars { display:grid; gap:.45rem; min-width:0; }
.barrow { display:grid; grid-template-columns:minmax(0,9.5rem) minmax(0,1fr) 3rem;
  gap:.5rem; align-items:center; font-size:.85rem; min-width:0; }
.barrow > span:first-child { overflow-wrap:anywhere; }
/* Both of these are spans. A grid item is blockified so .track picks up its
   width, but .fill is an ordinary inline child -- width and height simply do
   not apply to it until it is made a block, and every bar rendered at zero
   width. display:block is load-bearing here, not tidiness. */
.barrow .track { display:block; background:var(--n-100); height:1.1rem;
  border-radius:var(--r); overflow:hidden; }
.barrow .fill { display:block; background:var(--gold-ink); height:100%;
  min-width:2px; border-radius:var(--r); }
.barrow .fill.green { background:var(--green); }
.barrow .fill.red { background:var(--red); }
.barrow .v { text-align:right; font-weight:700; }

/* ── tables ────────────────────────────────────────────────────────────── */
/* Tables wrap (docs/responsive20261014.md Part 1, 2026-10-14). Every table
   used to be `min-width:max-content`: it refused to wrap, so each was as wide
   as its longest cell, and almost every page scrolled sideways inside its box
   -- Clinics & Camp by 209 px even at 1366. Now text cells wrap, and a long
   email, file name or code breaks rather than stretching its column. Numbers,
   dates, tags, chips and buttons stay on one line. Headers wrap between words.
   The sticky header went with it: it never stuck, because .tablewrap is the
   scroll box and the page is what scrolls. tests/test_layout.py holds this. */

.tablewrap { overflow-x:auto; -webkit-overflow-scrolling:touch; }
table { border-collapse:collapse; width:100%; font-size:.85rem; }
th, td { text-align:left; padding:.45rem .55rem; border-bottom:1px solid var(--n-200);
  vertical-align:top; }
/* break-word, not anywhere, on the cell: `anywhere` also lowers the width a
   column may be squeezed to, and the table then squeezed its short columns
   first -- "Clinic 1" and a date broken over two lines at 1024 px. A word
   still breaks if it alone is wider than its column; a code or file name
   may break anywhere. */
td { overflow-wrap:break-word; }
td code, td .mono { overflow-wrap:anywhere; }
th { font-size:.7rem; text-transform:uppercase; letter-spacing:.06em;
  color:var(--n-700); background:var(--paper); }
tbody tr:hover { background:var(--n-100); }
td.num, th.num { text-align:right; }
/* Below a PC a ten-column table needs the room its cell padding takes. */
@media (max-width:1023.98px) { th, td { padding-left:.4rem; padding-right:.4rem; } }
td.num, td.nowrap, th.nowrap, td .nowrap, td.num .mono, td.num code,
td .tag, td .chip, td .btn { white-space:nowrap; overflow-wrap:normal; }

/* A cell of buttons. On a tablet or PC they sit in a row and wrap -- and a
   long label ("Open for registration") may wrap inside its button, or that
   one label sets the width of the whole column; on a phone they stack, full
   width, each its own target. `.acts` elsewhere is a flex row, which a table
   cell must not become. */
td.acts { display:table-cell; }
td.acts form { display:inline; margin:0; }
td.acts .btn, td.acts .chip { margin:0 .3rem .3rem 0; white-space:normal; }
td.acts .note { display:block; }
/* A box to type in, inside a cell of buttons (Outcomes' "Why remove them?"),
   takes the column's width rather than setting it: its default 20 characters
   pushed a ten-column table past a tablet's edge. */
td.acts input:not([type=hidden]) { width:100%; min-width:0; box-sizing:border-box; }
/* "A phone", for tables, is below 740 px, as for the top bar and Menu: at 640
   px, landscape, Clinics & Camp, Learners and Outcomes' ten columns still
   overflowed their box by 66-115 px wrapped (measured 2026-10-14). */
@media (max-width:739.98px) {
  td.acts form { display:block; }
  td.acts .btn, td.acts .chip { display:block; width:100%; margin:0 0 .4rem;
    text-align:center; white-space:normal; }
}

/* A genuinely wide grid nobody reads on a field (the Documents register, Data
   issues, the report-card grid). On a phone it keeps its columns side by side
   and scrolls sideways, with a shadow at the edge that has more to show and a
   hint above it, so nobody misses the hidden columns. From 740 px up it wraps
   like every other table. The hint is the wrapper's `data-hint`, translated
   where the template writes it. It is laid out as on a tablet, 44rem, where
   all six fit wrapped: `max-content` put each switch's help text on one line
   and made the Switches table 3,100 px wide. */
@media (max-width:739.98px) {
  .tablewrap.wide > table { min-width:44rem; }
  .tablewrap.wide {
    background:
      linear-gradient(to right, var(--paper) 30%, transparent) left center / 2.5rem 100% no-repeat local,
      linear-gradient(to left, var(--paper) 30%, transparent) right center / 2.5rem 100% no-repeat local,
      radial-gradient(farthest-side at 0 50%, var(--n-300), transparent) left center / .9rem 100% no-repeat scroll,
      radial-gradient(farthest-side at 100% 50%, var(--n-300), transparent) right center / .9rem 100% no-repeat scroll;
  }
  .tablewrap.wide[data-hint]::before {
    content:attr(data-hint) " \2192"; display:block; position:sticky; left:0;
    padding:.3rem .55rem 0; font-size:.72rem; font-weight:600; color:var(--n-700);
  }
}

/* ── a row becomes a card on a phone (Part 2, 2026-10-14) ─────────────────
   Generalised from the match queue's cards (2026-10-07), which are now this.
   Below 740 px each row is a card: the first cell is its title, the rest are
   "Label: value" lines, and a cell of buttons (td.acts) is full width at the
   bottom. The labels are each cell's `data-label`, written from the same list
   as the header by templates/_table.html, so the two cannot drift. */
@media (max-width:739.98px) {
  table.cards, table.cards tbody, table.cards tr, table.cards td { display:block; width:auto; }
  table.cards thead { display:none; }
  table.cards tr { border-bottom:2px solid var(--n-300); padding:.35rem 0; }
  table.cards td { border-bottom:0; padding:.15rem .7rem; text-align:left; }
  table.cards td[data-label]:not([data-label=""]):not(:first-child)::before {
    content:attr(data-label) ":\00a0"; font-family:var(--face); font-size:.78rem; font-weight:700;
    color:var(--n-700); }
  table.cards td:first-child { font-size:.98rem; padding-top:.4rem; }
  table.cards td.acts { padding-top:.45rem; }
  /* A heading row inside the body (the Rules page's groups) stays a heading;
     the Rules table's 28% first column would make a card's title a sliver. */
  table.cards tbody th { display:block; }
  table.cards.rules td:first-child { width:auto; }
  /* A ranked board's first column is the place (`rank`): the card's title is
     "#3" and the learner beside it, not the place alone on a line. */
  table.cards td.rank:first-child, table.cards td.rank:first-child + td {
    display:inline-block; font-size:.98rem; padding-top:.4rem; padding-right:0; }
  table.cards td.rank:first-child::before { content:attr(data-label); }
  table.cards tr td.rank:first-child + td[data-label]::before { content:none; }
  /* A table of pairs, a question (th) and its answer: the question sits
     above its answer, each the full width, rather than both squeezed. */
  table.pairs tbody, table.pairs tr, table.pairs th, table.pairs td { display:block; }
  table.pairs th { border-bottom:0; padding-bottom:0; }
}

.tag {
  display:inline-block; font-size:.68rem; font-weight:700; padding:.1rem .35rem;
  border-radius:var(--r); border:1px solid var(--n-300); white-space:nowrap;
}
.tag.err  { background:#fdecea; border-color:var(--red); color:#8c1d16; }
.tag.warn { background:#fff6e0; border-color:var(--gold-ink); color:#6b4a00; }
.tag.info { background:#e8f4fb; border-color:var(--blue); color:#04537f; }
.tag.ok   { background:#e6f4ee; border-color:var(--green); color:#005636; }
/* A tag that is a phrase, not a code: it wraps, so "3 years young" does not
   set the width of a ten-column table or push past a phone's edge
   (age against grade, agegrade.py). Beats `td .tag`'s nowrap. */
.tag.wraps { white-space:normal; overflow-wrap:normal; }

/* A staff "Checked" on the learner page (age against grade, agegrade.py):
   the question on its own line, then the note and the button on one row,
   the same for both kinds. The note takes what is left; on a phone too narrow
   for both, the button drops under it rather than past the edge. */
.ag-check { margin-top:.45rem; }
.ag-check .lbl { margin-bottom:.2rem; }
.ag-check-row { display:flex; flex-wrap:wrap; gap:.4rem; align-items:center; }
.ag-check-row .in { flex:1 1 6rem; min-width:0; width:auto; }
.ag-check-row .chip { flex:none; }

code, .mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.82em; }

/* ── forms and filters ─────────────────────────────────────────────────── */

.filters { display:flex; flex-wrap:wrap; gap:.4rem; margin-bottom:.8rem; }
.filters input[type=search], .filters select {
  font-family:var(--face); font-size:.85rem; padding:.4rem .5rem;
  border:1px solid var(--n-300); border-radius:var(--r); background:var(--paper);
  min-width:0;
}
.chip {
  display:inline-block; padding:.32rem .6rem; border:1px solid var(--n-300);
  border-radius:var(--r); background:var(--paper); font-size:.8rem;
  text-decoration:none; font-weight:600;
}
.chip[aria-pressed="true"] { background:var(--ink); color:var(--paper); border-color:var(--ink); }

.btn {
  display:inline-block; background:var(--ink); color:var(--paper);
  border:0; border-radius:var(--r); padding:.55rem .9rem; font-family:var(--face);
  font-weight:700; font-size:.88rem; cursor:pointer; text-decoration:none;
}

/* ── login ─────────────────────────────────────────────────────────────── */

.login { min-height:100dvh; display:grid; place-items:center; background:var(--chrome);
  padding:1.2rem; }
.login form { width:min(24rem,100%); background:var(--paper); padding:1.4rem;
  border-radius:var(--r); border-top:var(--bar) solid var(--gold); }
.login input[type=password] { width:100%; font-family:var(--face); font-size:1rem;
  padding:.6rem; border:1px solid var(--n-300); border-radius:var(--r); }
.login .err { color:var(--red); font-size:.85rem; font-weight:600; margin:.5rem 0 0; }
.login img { height:34px; margin-bottom:.9rem; }

/* ── misc ──────────────────────────────────────────────────────────────── */

.kv { display:grid; grid-template-columns:minmax(0,auto) minmax(0,1fr);
  gap:.25rem .8rem; font-size:.86rem; }
.kv dd { overflow-wrap:anywhere; }
.kv dt { color:var(--n-700); font-weight:600; }
.kv dd { margin:0; }
.note { font-size:.82rem; color:var(--n-700); overflow-wrap:anywhere; }
.narrative { font-size:.85rem; background:var(--n-100); padding:.5rem .6rem;
  border-left:var(--bar) solid var(--n-300); border-radius:var(--r); white-space:pre-wrap; }
.empty { padding:1.5rem; text-align:center; color:var(--n-500); font-size:.9rem; }
.grid2 { display:grid; gap:1rem; min-width:0; }

/* ── breakpoints (verified at 360, 390, 768, 1024, 1280) ───────────────── */

@media (min-width:560px) {
  .tiles { grid-template-columns:repeat(3,1fr); }
  .tiles.four { grid-template-columns:repeat(4,1fr); }
}
@media (min-width:900px) {
  body { font-size:15px; }
  main { padding:1.2rem 1.2rem 4rem; }
  .tiles { grid-template-columns:repeat(6,1fr); }
  .grid2 { grid-template-columns:1fr 1fr; }
  h1 { font-size:1.7rem; }
}
@media print {
  nav.main, .topbar, .explain, .filters { display:none; }
  .panel { break-inside:avoid; border:1px solid #999; }
}

/* The language toggle. In the top bar it is a third select beside role and
   dataset; on the standalone applicant pages -- which a parent reaches before
   they have any session -- it is a plain pair of links, because two languages
   and one tap beats a dropdown. */
.langpill { border-color:var(--n-300); }
.langswitch {
  max-width:44rem; margin:0 auto; padding:.4rem .9rem 0;
  font-size:.82rem; text-align:right;
}
.langswitch a { color:var(--n-700); }
.langswitch span { font-weight:700; }
/* On the public form the toggle is a pair of submit buttons (it saves what has
   been typed before it switches), drawn to look exactly like the links. */
.langswitch button { background:none; border:0; padding:0; margin:0; font:inherit;
                     color:var(--n-700); text-decoration:underline; cursor:pointer; }
.langswitch .defaultsend { position:absolute; left:-9999px; width:1px; height:1px;
                           overflow:hidden; }


/* ── who built it ──────────────────────────────────────────────────────── */
/*
   A quiet credit rather than a co-brand. This is ACE's system and their name
   is at the top; ours belongs at the bottom, the same shape as the footer
   credit on warmupcoach.com, dpacheck.co.uk and the rest.
*/
.madeby {
  max-width:1280px; margin:0 auto; padding:1.4rem .9rem 2rem;
  display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;
  font-size:.76rem; color:var(--n-500);
  border-top:1px solid var(--n-200);
}
.madeby img { height:15px; width:auto; opacity:.85; }
.madeby a { color:var(--n-700); text-decoration:none; font-weight:600; }
.madeby a:hover, .madeby a:focus-visible { text-decoration:underline; }
.madeby .sep { opacity:.5; }

/* On the applicant-facing pages the credit sits inside the narrow sheet. */
.sheet .madeby { padding-left:0; padding-right:0; }

/* Inside the login card. The login page's ground is black, so a credit after
   the card would be grey on black and below the fold -- present in the markup
   and invisible to a reader, which is not a credit. */
.madeby-card { padding:.9rem 0 0; margin-top:.9rem; font-size:.72rem; }
/* .login img is the 34px ACE wordmark. The tally mark in the credit is not
   that, and inherits it without this. */
.login .madeby img { height:12px; margin:0; }

/* ==========================================================================
   The guided tour.

   Deliberately not a spotlight-with-a-hole. A cut-out backdrop means either an
   SVG mask or four positioned panels, both of which have to be recomputed on
   every scroll and both of which drift by a pixel on a zoomed display. A dim
   layer plus a raised ring reads the same and cannot drift.
   ========================================================================== */

/* The button. A bare "?" read as help-if-you-are-stuck; a named control with a
   figure on it reads as an offer, which is what it is. */
.tour-btn {
  flex:none; display:inline-flex; align-items:center; gap:.35rem;
  margin-right:.7rem; padding:.28rem .6rem .28rem .45rem;
  border:1px solid var(--gold); border-radius:999px;
  background:transparent; color:var(--gold); cursor:pointer;
  font-family:var(--face); font-size:.68rem; font-weight:700;
  text-transform:uppercase; letter-spacing:.1em; line-height:1;
}
.tour-btn .runner { width:15px; height:16px; flex:none; }
.tour-btn:hover { background:var(--gold); color:var(--ink); }
/* He shifts his weight on hover -- enough to suggest he is about to set off,
   not enough to be a cartoon. */
.tour-btn:hover .runner { transform:translateX(1px) translateY(-1px); }
.tour-btn .runner { transition:transform .18s ease; }

/* Six poses in the one file, one shown at a time. Stride A is the resting pose
   -- it is what the button shows and what he lands in -- and tour.js swaps in
   an event for the length of each leap by setting data-move on the container.
   Listing the pairs out longhand rather than being clever about it: CSS cannot
   match a child's class against its parent's attribute, and five explicit
   lines are easier to follow than any trick that would avoid them. */
.runner .r-b, .runner .r-hurdle, .runner .r-long,
.runner .r-flop, .runner .r-jav { display:none; }

.tour-runner[data-move] .runner .r-a { display:none; }
.tour-runner[data-move="sprint"]   .runner .r-b      { display:block; }
.tour-runner[data-move="hurdle"]   .runner .r-hurdle { display:block; }
.tour-runner[data-move="longjump"] .runner .r-long   { display:block; }
.tour-runner[data-move="flop"]     .runner .r-flop   { display:block; }
.tour-runner[data-move="javelin"]  .runner .r-jav    { display:block; }

/* The javelin, thrown ahead of him and landing on whatever the tour is
   pointing at. Its own element rather than a line inside the runner, because
   the whole point is that it leaves him and arrives somewhere else. */
.tour-spear {
  display:none; position:fixed; z-index:81; pointer-events:none;
  width:34px; height:7px; color:var(--gold); opacity:0;
  filter:drop-shadow(0 3px 8px rgba(0,0,0,.5));
}
body.tour-on .tour-spear.placed { display:block; }
.tour-spear svg { width:100%; height:100%; display:block; }

.tour-runner {
  display:none; position:fixed; z-index:81; pointer-events:none;
  width:44px; height:47px; color:var(--gold);
  filter:drop-shadow(0 4px 10px rgba(0,0,0,.55));
  transition:left .01s, top .01s;
}
body.tour-on .tour-runner.placed { display:block; }
.tour-runner .runner { width:100%; height:100%; }

.tour-back, .tour-ring, .tour-box { display:none; }

body.tour-on .tour-back {
  display:block; position:fixed; inset:0; z-index:80;
  background:rgba(0,0,0,.55);
}
body.tour-on .tour-ring {
  display:block; position:fixed; z-index:81; pointer-events:none;
  border:2px solid var(--gold); border-radius:var(--r);
  box-shadow:0 0 0 3px rgba(255,182,18,.28);
  transition:top .22s ease, left .22s ease, width .22s ease, height .22s ease;
}
body.tour-on .tour-box {
  display:block; position:fixed; z-index:82;
  background:var(--paper); color:var(--ink); border-radius:var(--r);
  border-top:var(--bar) solid var(--gold);
  box-shadow:0 18px 44px rgba(0,0,0,.3);
  padding:1rem 1.1rem 1.1rem;
}
.tour-count {
  margin:0 0 .35rem; font-size:.62rem; font-weight:700;
  letter-spacing:.12em; text-transform:uppercase; color:var(--n-500);
}
.tour-box h3 { margin:0 0 .4rem; font-size:1rem; }
.tour-body { margin:0; font-size:.86rem; line-height:1.55; color:var(--n-700); }
.tour-nav { display:flex; align-items:center; gap:.5rem; margin-top:1rem; }
.tour-nav .tour-sp { flex:1; }
.tour-nav button {
  font-family:var(--face); font-size:.8rem; cursor:pointer;
  border-radius:var(--r); padding:.4rem .8rem;
}
.tour-skip, .tour-prev {
  border:1px solid var(--n-300); background:var(--paper); color:var(--n-700);
}
.tour-skip { border-color:transparent; padding-left:0; }
.tour-next { border:1px solid var(--ink); background:var(--ink); color:var(--paper); font-weight:700; }

/* The ring is decoration; on a phone the box is the tour. Pin it to the bottom
   so it never lands under a thumb-height target it is trying to point at. */
@media (max-width:560px) {
  body.tour-on .tour-box {
    top:auto !important; bottom:12px; left:12px !important;
    width:calc(100vw - 24px) !important;
  }
}
@media (prefers-reduced-motion:reduce) {
  body.tour-on .tour-ring { transition:none; }
}
.tour-list { list-style:none; margin:.6rem 0 0; padding:0; }
.tour-list li {
  position:relative; padding:.3rem 0 .3rem .9rem;
  font-size:.83rem; line-height:1.5; color:var(--n-700);
}
.tour-list li::before { content:"\2014"; position:absolute; left:0; color:var(--gold-ink); }

/* Something not built yet says so ("Sending to a group" on Communication).
   The "experimental" tags on APE, in the menu and on the page, came off
   2026-10-06 (Paul). */
.tag.exp { background:#f3eefb; border-color:#7048b6; color:#4c2d80; text-transform:none; }

/* ── Proposal | Demo ───────────────────────────────────────────────────────
   The same control as the one at the top of the pitch, inverted for the dark
   topbar: there the current page is ink on paper, here it is paper on ink and
   the frame has to be visible against black. Shape, size and letter-spacing
   are deliberately identical -- it is one deliverable in two halves, and a
   switch that changed appearance when you crossed between them would read as
   two different products. */
.topbar .switch {
  display:inline-flex; flex:none; margin-right:.9rem;
  border:1px solid var(--n-500); border-radius:var(--r); overflow:hidden;
}
.topbar .switch > * {
  font-family:var(--face); font-size:.62rem; font-weight:700;
  text-transform:uppercase; letter-spacing:.1em; line-height:1.2;
  padding:.5em .9em; white-space:nowrap; text-decoration:none;
  display:inline-flex; align-items:center;
  color:var(--n-300); background:transparent;
}
.topbar .switch > * + * { border-left:1px solid var(--n-500); }
.topbar .switch [aria-current="page"] { background:var(--paper); color:var(--ink); cursor:default; }
.topbar .switch a:not([aria-current]):hover { background:var(--gold); color:var(--ink); }

/* ── Communication ─────────────────────────────────────────────────────────
   The page looks like a working mail tool, so the caveat leads and the Send
   button is visibly dead. Paul, 2026-09-09: there must be no doubt this is not
   operational. */
.caveat {
  margin:0 0 1rem; padding:.7rem .85rem; font-size:.86rem; line-height:1.5;
  background:var(--tint-red); border-left:var(--bar) solid var(--red); border-radius:var(--r);
}
.caveat strong { color:var(--red-ink); }

.chan { display:flex; align-items:center; gap:1rem; flex-wrap:wrap; margin:0 0 .9rem; }
.chan label { font-size:.86rem; font-weight:600; cursor:pointer; }

.lbl {
  display:block; font-size:.62rem; font-weight:700; text-transform:uppercase;
  letter-spacing:.12em; color:var(--n-700); margin-bottom:.25rem;
}
.in {
  width:100%; font-family:var(--face); font-size:.9rem; padding:.5rem .6rem;
  border:1px solid var(--n-300); border-radius:var(--r); background:var(--paper);
  color:var(--ink);
}
.in:focus { outline:2px solid var(--gold); outline-offset:1px; }
textarea.in { line-height:1.55; resize:vertical; }

.note.over { color:var(--red-ink); font-weight:600; }

.acts { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; }
/* Disabled and looking it. A Send that could be mistaken for live is worse
   than no Send at all. */
.btn.off {
  background:var(--n-200); border-color:var(--n-300); color:var(--n-500);
  cursor:not-allowed; text-decoration:line-through;
}
tr.on td { background:var(--tint-amber); }

/* ── clinic charts (2026-09-17) ─────────────────────────────────────────────
   Bars as flex rows rather than SVG: the labels are long, in two languages,
   and need to wrap and be selectable, which is fiddly in SVG and free here.
   Marks stay thin, ends are rounded, and every bar is direct-labelled so
   identity never rests on colour alone. --gold-ink and --green are the two
   brand steps that pass CVD separation and 3:1 contrast on this surface; the
   brand gold is 1.71:1 and is deliberately not used as a data mark. */
.chart { display:flex; flex-direction:column; gap:.45rem; margin-top:.2rem; }
.cbar { display:grid; grid-template-columns:8.5rem 1fr 4.5rem; gap:.6rem;
        align-items:center; min-width:0; }
.cbar-l { font-size:.8rem; color:var(--n-700); min-width:0; overflow-wrap:anywhere; }
.cbar-t { background:var(--n-100); border-radius:var(--r); overflow:hidden;
          display:flex; flex-direction:column; gap:2px; min-height:.7rem; }
.cbar-f { display:block; height:.7rem; border-radius:0 var(--r) var(--r) 0;
          background:var(--n-500); min-width:2px; }
.cbar-pair .cbar-f { height:.55rem; }
.cbar-v { font-size:.8rem; font-weight:700; text-align:right; white-space:nowrap; }
/* Unknown is not an error: grey, never red. It is a gap in the data, and the
   note under the chart says why rather than the colour implying blame. */
.cbar-unknown .cbar-l, .cbar-unknown .cbar-v { color:var(--n-500); }
.cbar-unknown .cbar-f { background:var(--n-300); }
.chart-key { display:flex; gap:1rem; margin:.1rem 0 .5rem; font-size:.78rem;
             color:var(--n-700); }
.chart-key .key { display:inline-flex; align-items:center; gap:.35rem; }
.chart-key i { width:.7rem; height:.7rem; border-radius:var(--r); display:block; }
@media (max-width:560px) { .cbar { grid-template-columns:6rem 1fr 3.6rem; } }

/* ── the batching options (2026-09-17) ──────────────────────────────────────
   Cards rather than a list of disabled buttons. Nothing here is clickable and
   nothing should LOOK clickable: these are five shapes a group send could
   take, for ACE to choose between, and a greyed-out button invites a click and
   then disappoints. */
.batches { display:grid; gap:.8rem; margin-top:.9rem; }
@media (min-width:820px) { .batches { grid-template-columns:1fr 1fr; } }
.batch { border:1px solid var(--n-200); border-radius:var(--r);
         padding:.8rem .9rem; background:var(--bone); min-width:0; }
.batch-rec { border-left:var(--bar) solid var(--gold); background:var(--paper); }
.batch h3 { font-size:.92rem; margin:0 0 .35rem; }
.batch-s { font-size:.86rem; color:var(--n-700); margin:0 0 .5rem; }
.batch .kv { margin:0; font-size:.8rem; }
.caveat-part { background:var(--tint-amber-2); color:var(--ink); border-left:var(--bar) solid var(--gold); }

/* A group member the allowlist currently refuses. Struck through rather than
   hidden: the point is that ACE can SEE the audience and the guard at the same
   time. Grey, not red -- nothing is wrong, the policy simply is not agreed. */
.blocked { text-decoration:line-through; color:var(--n-500); }

/* Assessments, batched by domain — added 2026-09-20.
   Two columns of groups on a wide screen, one on a narrow. `break-inside` is
   load-bearing: without it a group splits across the column boundary and its
   heading ends up orphaned above a different group's rows, which is worse than
   no grouping at all. */
/* `min(100%, 21rem)`: a fixed 21rem (336 px) column in a 300 px panel was the
   one page measured spilling off a phone (responsive20261014.md, 2026-10-14). */
.agroups { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,21rem),1fr));
  gap:1.1rem 1.6rem; align-items:start; }
/* A group carrying prose takes two columns where there is room. One column is
   too narrow for a paragraph about a child and reads worse than no grouping. */
.agroup.haswords { grid-column:span 2; }
@media (max-width:52rem) { .agroup.haswords { grid-column:span 1; } }
.agroup { min-width:0; }
.agroup h3 { font-size:.72rem; text-transform:uppercase; letter-spacing:.11em;
  color:var(--n-600); font-weight:700; margin:0 0 .45rem;
  padding-bottom:.3rem; border-bottom:var(--bar) solid var(--gold); }
.agroup .kv { align-items:baseline; }
.agroup .kv dt { grid-column:1; }
.agroup .kv dd { grid-column:2; }
.agroup .narrative { margin-top:.15rem; }
.agroup .note { margin-top:.2rem; }

.atext { margin-top:.55rem; }
.alabel { display:block; color:var(--n-700); font-weight:600; font-size:.86rem;
  margin-bottom:.2rem; }

/* Capture score, RAG-banded — 2026-09-20.
   Colour AND a number, never colour alone: about 1 in 12 men has some degree
   of red-green colour blindness, and a red/green pill with nothing else in it
   is invisible to them. The number carries the meaning; the colour is the
   thing you can scan for. */
.rag { display:inline-block; font-weight:700; font-size:.82rem;
  padding:.1rem .45rem; border-radius:var(--r); margin-left:.4rem;
  border:1px solid transparent; cursor:help; }
.rag-of { font-weight:400; opacity:.65; font-size:.74rem; }
.rag-green { background:#e6f4ea; color:#14532d; border-color:#b7e0c4; }
.rag-amber { background:#fdf3d8; color:#6b4b09; border-color:#f0dca6; }
.rag-red   { background:#fbe4e4; color:#7a1a1a; border-color:#f2bcbc; }
.rag-grey  { background:var(--n-100); color:var(--n-700); border-color:var(--n-300); }
/* The other-language name: present, but never competing with the printed one. */
.xlate { color:var(--n-600); font-size:.8rem; margin-left:.35rem; }
.xlate::before { content:"· "; }
/* A Totaal line or an SBA weighting is shown but demoted — it is on the page,
   so hiding it would make the screen disagree with the paper, but it is not a
   subject and is not in the average. */
tr.notsubject td { opacity:.6; font-style:italic; }

/* Faces — 2026-09-23.
   Served through /document/<id>, so a photograph obeys the same role gate and
   audit as every other document. `object-fit: cover` matters: these are phone
   snaps at every aspect ratio, and without it a portrait squashes into the
   circle and a landscape one stretches. */
.whoami { display:flex; align-items:center; gap:.5rem; text-decoration:none; }
.avatar { width:32px; height:32px; border-radius:50%; object-fit:cover;
  background:var(--n-100); flex:0 0 32px; }
.portrait { width:96px; height:96px; border-radius:50%; object-fit:cover;
  background:var(--n-100); border:var(--bar) solid var(--gold); }
/* Initials, not an empty circle: "no photograph uploaded" and "this role may
   not see it" must not look like a broken image. */
.avatar.noface, .portrait.noface { display:inline-flex; align-items:center;
  justify-content:center; font-weight:700; color:var(--n-600);
  background:var(--n-100); text-transform:uppercase; letter-spacing:.02em; }
.avatar.noface { font-size:.7rem; }
.portrait.noface { font-size:1.6rem; border-color:var(--n-200); }
.namerow { display:flex; align-items:center; gap:1rem; margin-bottom:.4rem; }
.namerow h1 { margin:0; }

/* ── Staff-side capture, 2026-09-25 ────────────────────────────────────────
   Two boxes, and the difference between them is load-bearing. `.problems`
   means nothing was saved; `.warnings` means it will be saved once somebody
   ticks the box. The public form has its own copy of `.problems` inline
   (forms_render.html) because it does not extend base.html. */
.problems, .warnings {
  border-radius:var(--r); padding:.65rem .8rem; margin:.9rem 0;
  font-size:.86rem;
}
.problems { background:var(--tint-red); border:1px solid var(--red); }
.warnings { background:var(--tint-amber); border:1px solid var(--gold-ink); }
.problems ul, .warnings ul { margin:.35rem 0 0; padding-left:1.1rem; }
.problems li, .warnings li { margin:.2rem 0; }
.warnings .opt { display:block; margin-top:.55rem; font-weight:600; }
.req { color:var(--red); }
/* A control that exists but not here. Said rather than hidden, because a
   missing button on a read-only dataset looks identical to a feature nobody
   built. */
.readonly-note {
  border:1px dashed var(--n-300); border-radius:var(--r); background:var(--n-100);
  padding:.5rem .7rem; font-size:.82rem; color:var(--n-700);
}

/* An alert panel: something on this record needs a person to act. Red rule
   rather than a red fill, so the panel still reads as part of the record and
   the words carry the urgency. */
.panel.alert { border-left:var(--bar) solid var(--red); background:var(--tint-red-soft); }
.panel.alert ul { margin:.4rem 0 .6rem; padding-left:1.1rem; font-size:.88rem; }
.panel.alert li { margin:.25rem 0; }

/* ── the Rules table (2026-10-02) ──────────────────────────────────────────
   One table, grouped. It wrapped before every table did (Part 1, 2026-10-14). */
table.rules { table-layout:auto; }
table.rules td { vertical-align:top; white-space:normal; }
table.rules td:first-child { width:28%; }
table.rules td.nowrap { white-space:nowrap; }
table.rules tr.group th { background:var(--n-100); color:var(--ink); white-space:normal;
  font-size:.72rem; padding-top:.6rem; }
table.rules details table { margin-top:.3rem; }
table.rules details summary { cursor:pointer; }

/* ── The coach's block on the learner page (stage 4 of docs/clinics.md) ─────
   One block per clinic registration. Everything wraps: at 360 px a result
   line is the test picker over the value, and a long test name ("Following of
   instructions (rating 1–5)") is truncated inside its select rather than
   pushing the page sideways. */
.cblock { border-top:1px solid var(--n-200); padding:.8rem 0 .4rem; }
.cblock:first-of-type { border-top:0; }
.cblock h3 { margin:0 0 .15rem; font-size:1rem; }
.cblock .cb-when { margin:0 0 .5rem; }
.cb-att { border:0; padding:0; margin:0 0 .6rem; display:flex; flex-wrap:wrap;
  gap:.2rem 1rem; }
.cb-att legend { padding:0; margin-bottom:.25rem; }
.cb-att .opt { font-size:.88rem; cursor:pointer; }
.cb-bib { max-width:8rem; display:block; }
.cb-results { list-style:none; margin:.2rem 0 .6rem; padding:0; }
.cb-results li { display:flex; flex-wrap:wrap; align-items:baseline; gap:.2rem .6rem;
  padding:.35rem 0; border-bottom:1px dashed var(--n-200); font-size:.88rem; }
.cb-results li > * { min-width:0; overflow-wrap:anywhere; }
.cb-test { font-weight:600; }
.cb-fix { flex-basis:100%; }
.cb-fix summary, .cb-add summary { cursor:pointer; font-size:.82rem; color:var(--n-700); }
.cb-fix .in { max-width:14rem; display:block; margin:.25rem 0; }
/* Test, value, and the value's unit badge (campfixes Part 5: an auto third
   column, empty while the badge is hidden). "Timed by" takes a row of its own
   so a hidden badge cannot pull it up into the badge's column. */
.cb-line { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,8rem) auto;
  gap:.3rem .5rem; align-items:end; margin:.45rem 0; }
.cb-line > .lbl, .cb-line > .cb-basis { grid-column:1 / -1; margin:0; }
.cb-line select.in { min-width:0; }
.cb-unit { align-self:center; padding:.1rem .45rem; border:1px solid var(--n-200);
  border-radius:var(--r); font-size:.82rem; color:var(--n-700); white-space:nowrap; }
.cb-unit[hidden] { display:none; }
.cb-add { margin:.5rem 0; }
@media (max-width:420px) {
  /* The test on its own row; the value and its badge share the next. */
  .cb-line { grid-template-columns:minmax(0,1fr) auto; }
  .cb-line > select.in { grid-column:1 / -1; }
}

/* ── APE section of the learner page (docs/ape.md step 6) ─────────────────
   Inline SVG drawn by templates/_ape_section.html from apeviews.py. Tokens
   only, so the dark theme redraws it. Series colours (progress chart) are in
   a fixed order checked for colour-blind separation (green, gold, blue, red);
   every series also has its own marker shape and a legend entry. */
.ape-h { font-size:.95rem; margin:1rem 0 .4rem; }
.ape-dials { display:grid; grid-template-columns:repeat(auto-fill, minmax(min(100%, 210px), 1fr));
  gap:.8rem; }
.ape-dial { margin:0; padding:.5rem; border:1px solid var(--n-200); border-radius:var(--r);
  background:var(--paper); min-width:0; }
.ape-overall { border-color:var(--gold-ink); }
.ape-dial svg { display:block; width:100%; height:auto; overflow:visible; }
.ape-dial-name { font-weight:700; font-size:.9rem; overflow-wrap:anywhere; }
.ape-big { font-size:2rem; font-weight:700; line-height:1.1; margin:.1rem 0 0; text-align:center; }
.ape-sub { text-align:center; margin:.15rem 0; font-size:.84rem; }
.ape-track { fill:none; stroke:var(--n-200); }
.ape-value { fill:none; stroke:var(--blue); }
.ape-zone { fill:none; }
.ape-zone-below, .ape-zone-above { stroke:var(--n-200); }
.ape-zone-about { stroke:var(--gold); }
.ape-tick { stroke:var(--ink); stroke-width:2; }
.ape-ticklabel, .ape-zonelabel { fill:var(--n-700); font-family:var(--face); font-size:10px; }
.ape-zonelabel { font-weight:700; }
.ape-needle { stroke:var(--ink); stroke-width:3; stroke-linecap:round; }
.ape-pivot { fill:var(--ink); }
.ape-key, .ape-source, .ape-thin { margin:.35rem 0; }
.ape-bars { display:grid; gap:.45rem; }
.ape-bar-row { display:grid; grid-template-columns:minmax(0,9rem) minmax(0,1fr) 3rem;
  gap:.2rem .6rem; align-items:center; }
.ape-bar-name { font-weight:600; font-size:.88rem; overflow-wrap:anywhere; }
.ape-bar { display:block; width:100%; height:18px; }
.ape-bar-track { fill:var(--n-100); }
.ape-bar-fill { fill:var(--blue); }
.ape-bar-mark { stroke:var(--n-700); stroke-width:1.5; opacity:.75; }
.ape-bar-pts { font-weight:700; text-align:right; }
.ape-bar-flags { grid-column:2 / -1; }
.ape-bar-none { grid-column:2 / -1; }
.ape-flag { margin:.1rem .2rem 0 0; text-transform:none; }
.ape-chart { display:block; width:100%; max-width:760px; height:auto; }
.ape-grid { stroke:var(--n-200); }
.ape-axis { stroke:var(--n-500); }
.ape-line { fill:none; stroke:currentColor; stroke-width:2; stroke-linejoin:round; }
.ape-medal { stroke:currentColor; stroke-width:1.2; stroke-dasharray:6 4; opacity:.8; }
.ape-dot { fill:currentColor; stroke:var(--paper); stroke-width:2; }
.ape-s1 { color:var(--green); }
.ape-s2 { color:var(--gold-ink); }
.ape-s3 { color:var(--blue); }
.ape-s4 { color:var(--red); }
.ape-s5 { color:var(--n-700); }
.ape-s6 { color:var(--blue-deep); }
.ape-legend { list-style:none; padding:0; margin:.4rem 0; display:flex; flex-wrap:wrap;
  gap:.3rem 1rem; font-size:.86rem; }
.ape-legend li { display:flex; align-items:center; gap:.35rem; }
.ape-best td { background:var(--tint-gold); }
.ape-maturity { margin-top:.8rem; }
@media (max-width:560px) {
  .ape-bar-row { grid-template-columns:minmax(0,1fr) 3rem; }
  .ape-bar-name { grid-column:1 / -1; }
  .ape-bar-flags, .ape-bar-none { grid-column:1 / -1; }
}

/* ── match queue (2026-10-07) ─────────────────────────────────────────────
   The tiles are the filter; the pressed one is outlined. Each registration
   lists up to three suggestions, each with its own Confirm. */
a.tile { display:block; color:inherit; text-decoration:none; }
a.tile:hover { border-color:var(--n-300); }
a.tile[aria-pressed="true"] { outline:2px solid var(--ink); outline-offset:-2px; }
/* The long "name similarity …" reasons once pushed Other options off the
   right edge (Paul's screenshot, 2026-10-07), so these tables have fixed
   column widths. Under 740px each row is a card: they carry `cards` as well
   (the shared pattern in the tables section, generalised from these). */
table.match-table { table-layout:fixed; }
.match-table col.c-reg { width:26%; }
.match-table col.c-opt { width:13.5rem; }
.match-table col.c-res { width:22%; }
.match-table .opt-stack form .chip { width:100%; text-align:center; }
.pick-form input { flex:1 1 8rem; }
.cand-list { list-style:none; margin:0; padding:0; }
.cand { display:flex; gap:.6rem; justify-content:space-between; align-items:flex-start;
  padding:.4rem 0; border-top:1px solid var(--n-200); }
.cand:first-child { border-top:0; padding-top:0; }
.cand form { flex:none; }
.match-table td form { margin:0 0 .35rem; }
.pick-form { display:flex; flex-wrap:wrap; gap:.3rem; align-items:center; }
.pick-form label { flex-basis:100%; }
.pick-form input { min-width:0; max-width:100%; }
.btn:disabled, .btn[disabled], .chip:disabled { opacity:.45; cursor:not-allowed; }

/* match.js: a row collapses and the rows below slide up; a notice at the
   bottom says what happened, with Undo. Tokens only, so dark mode follows. */
.mq-wrap { overflow:hidden; transition:height .35s ease; }
.match-table tr.mq-leaving { opacity:.35; transition:opacity .2s ease; }
.match-table td.mq-flat { padding-top:0; padding-bottom:0; border-bottom-width:0;
  transition:padding .35s ease; }
.match-table tr.mq-back { background:var(--n-100); transition:background 1.4s ease; }
.mq-toast { position:fixed; left:0; right:0; bottom:1rem; margin:0 auto;
  width:fit-content; transform:translateY(150%);
  z-index:50; display:flex; gap:.8rem; align-items:center; max-width:min(42rem, calc(100vw - 2rem));
  background:var(--ink); color:var(--paper); border-radius:var(--r);
  padding:.65rem .8rem; font-size:.88rem; box-shadow:0 6px 24px rgba(0,0,0,.25);
  transition:transform .25s ease; }
.mq-toast.mq-show { transform:translateY(0); }
.mq-toast.mq-toast-err { border-left:4px solid var(--red); }
.mq-toast button { background:none; border:0; color:inherit; font:inherit; cursor:pointer; }
.mq-toast .mq-undo { font-weight:700; text-decoration:underline; white-space:nowrap; }
.mq-toast .mq-close { font-size:1.2rem; line-height:1; opacity:.7; }
@media (prefers-reduced-motion: reduce) {
  .mq-wrap, .mq-toast, .match-table td.mq-flat, .match-table tr.mq-leaving { transition:none; }
}

/* merges (2026-10-07): the side-by-side. Rows that differ are tinted and
   ask for a choice; the rest read as one value. */
.merge-table th[scope], .merge-table tbody th { width:9rem; color:var(--n-700); font-weight:600; }
.merge-table td { white-space:normal; overflow-wrap:break-word; word-break:normal; }
@media (max-width:560px) {
  .merge-table tbody th { width:5.2rem; font-size:.62rem; padding-left:.3rem; padding-right:.3rem; }
  .merge-table td { padding-left:.35rem; padding-right:.35rem; }
  .mq-dialog { padding:.7rem; }
}
.merge-table tr.merge-differs td { background:var(--n-100); }
.merge-table tr.merge-differs label { display:flex; gap:.4rem; align-items:flex-start; cursor:pointer; }
.merge-table tr.merge-sum td, .merge-table tr.merge-sum th { border-top:2px solid var(--n-200); }
.merge-actions { display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; margin-top:.8rem; }
.merge-line { display:flex; flex-wrap:wrap; gap:.6rem; align-items:center;
  justify-content:space-between; padding:.4rem 0; border-top:1px solid var(--n-200); }
.merge-line:first-of-type { border-top:0; }
.mq-dialog { border:1px solid var(--n-300); border-radius:var(--r); padding:1rem;
  background:var(--paper); color:var(--ink); width:min(46rem, calc(100vw - 2rem));
  max-height:calc(100vh - 2rem); overflow:auto; }
.mq-dialog::backdrop { background:rgba(0,0,0,.45); }
.mq-dialog h2 { margin-top:0; }

/* ── Performance panel (performance.py, 2026-10-07) ─────────────────────────
   The learner's headline: overall gauge + where they rank, a gauge per APE
   event, coach ratings as pips, other tests as a "better than X%" meter.
   Tokens only, so dark mode follows. */
.perf-hero { display:grid; grid-template-columns:minmax(0, 340px) minmax(0, 1fr); gap:1.2rem;
  align-items:center; padding:.4rem 0 .8rem; border-bottom:1px solid var(--n-200); }
.perf-gauge { margin:0; }
.perf-gauge svg { display:block; width:100%; height:auto; overflow:visible; }
.perf-kicker { margin:0; font-size:.72rem; text-transform:uppercase; letter-spacing:.08em;
  color:var(--n-700); font-weight:700; }
.perf-hero-n { margin:.1rem 0; font-size:3.2rem; font-weight:700; line-height:1; }
.perf-band { margin:.2rem 0 .5rem; font-weight:600; }
.perf-band-above { color:var(--blue-deep); }
.perf-rank { margin:.2rem 0 .4rem; font-size:1.05rem; }
.perf-h { margin:1rem 0 .5rem; font-size:.78rem; text-transform:uppercase;
  letter-spacing:.08em; color:var(--n-700); }
.perf-events { display:grid; gap:.7rem;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, 250px), 1fr)); }
.perf-event { margin:0; padding:.6rem .6rem .5rem; border:1px solid var(--n-200);
  border-radius:var(--r); background:var(--paper); text-align:center; }
.perf-event figcaption { font-weight:700; text-align:left; }
.perf-event svg { display:block; width:100%; height:auto; overflow:visible; }
.perf-n { margin:.1rem 0 0; font-size:1.8rem; font-weight:700; line-height:1.1; }
.perf-of { font-size:.85rem; font-weight:400; color:var(--n-700); }
.perf-sub { margin:.15rem 0 0; font-size:.82rem; }
.perf-level { font-weight:700; }
.perf-level.up { color:var(--blue-deep); }
.perf-cols { display:grid; gap:1.2rem; grid-template-columns:repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.perf-list { list-style:none; margin:0; padding:0; }
.perf-list li { display:grid; grid-template-columns:minmax(6rem, 9rem) auto 1fr; gap:.3rem .7rem;
  align-items:center; padding:.45rem 0; border-top:1px solid var(--n-200); }
.perf-list li:first-child { border-top:0; }
.perf-list .perf-sub { grid-column:2 / -1; margin:0; color:var(--n-700); }
.perf-label { font-weight:600; }
.perf-val { font-weight:700; white-space:nowrap; }
.pips { display:inline-flex; gap:.25rem; }
.pip { width:.9rem; height:.9rem; border-radius:50%; background:var(--n-200); }
.pip.on { background:var(--blue); }
.meter { position:relative; display:block; height:.55rem; border-radius:.3rem;
  background:var(--n-200); overflow:hidden; min-width:4rem; }
.meter-fill { position:absolute; left:0; top:0; bottom:0; border-radius:.3rem;
  background:var(--blue); }
.perf-links { margin:1rem 0 0; font-size:.88rem; }
@media (max-width:640px) {
  .perf-hero { grid-template-columns:1fr; }
  .perf-gauge { max-width:300px; margin:0 auto; }
}

/* ── touch screens (docs/responsive20261014.md Part 6, 2026-10-14) ────────
   Buttons and chips measured 31-36 px tall, under the 40-44 px a finger
   needs, and the form builder's ticks 13 px. On a touch screen only: a PC
   with a mouse keeps today's compact sizes. Kept at the end so it wins over
   the rules it enlarges. */
@media (pointer:coarse) {
  .btn, .chip, .tour-btn, .topdrop > summary, .theme-toggle button,
  .langswitch a, .langswitch button:not(.defaultsend), .topbar a.out, .topbar .switch > * {
    min-height:44px; display:inline-flex; align-items:center; justify-content:center;
  }
  .in, select, .rolepill, .topbar a.home,
  input:is([type=text], [type=search], [type=email], [type=tel], [type=number],
           [type=date], [type=password], :not([type])) { min-height:44px; }
  textarea.in { min-height:5rem; }
  summary { min-height:44px; align-content:center; }
  nav.main a { min-height:44px; display:flex; align-items:center; }
  .menu-grid a { min-height:44px; }
  input[type=checkbox], input[type=radio] { width:24px; height:24px; flex:none; }
}
@media (pointer:coarse) and (max-width:739.98px) {
  td.acts .btn, td.acts .chip { display:flex; }
}

/* The APE progress chart on a phone: its labels are SVG text in a 600-unit
   viewBox drawn about 300 px wide, so 10 units was 4-5 px. Twice the size,
   the sparser set of dates (apeviews `narrow`), and room to overhang. */
@media (max-width:599.98px) {
  .ape-chart { overflow:visible; }
  .ape-chart .ape-ticklabel { font-size:24px; }
  .ape-chart .ape-wide-only { display:none; }
}

/* ── Part 4: APE pick-list and form builder ── */
/* docs/responsive20261014.md Part 4 (2026-10-14). Two pages that wrapping
   could not fix: the APE grid (about 200 columns) and the form builder (seven
   columns of inputs per question). Breakpoints as the plan: phone < 600px,
   tablet 600–1023px, PC ≥ 1024px. Everything in this block, nothing above. */

/* The APE page: pick an event, age, sex and timing (a plain GET form), read
   one two-column table. The whole grid is the CSV download beneath it. */
.ape-pick-form { display:grid; gap:.5rem .7rem; align-items:end; margin:.6rem 0 1rem;
  grid-template-columns:repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr)); max-width:48rem; }
.ape-pick-form label { min-width:0; }
.ape-pick-form .btn { justify-self:start; }
table.ape-points { width:100%; max-width:22rem; min-width:0; font-size:.92rem; }
table.ape-points td, table.ape-points th { padding:.3rem .6rem; }
table.ape-points td.mono { font-size:.95em; white-space:nowrap; }
.ape-downloads { display:flex; flex-wrap:wrap; gap:.4rem; margin:.8rem 0 0; }

/* The form builder: one block per question, four lines (forms_edit.html).
   No box here is wider than its panel, so nothing scrolls sideways. */
table.fb-changes { min-width:0; }
table.fb-changes td.note { white-space:normal; overflow-wrap:anywhere; }
.fb-qs { display:grid; gap:.6rem; }
.fb-q { display:grid; gap:.5rem; padding:.6rem .7rem; min-width:0;
  border:1px solid var(--n-200); border-radius:var(--r); background:var(--paper); }
.fb-q.fb-new { border-style:dashed; border-color:var(--n-300); background:var(--n-100); }
.fb-q .in { font-size:.88rem; padding:.35rem .5rem; }
.fb-line { display:grid; gap:.45rem .6rem; align-items:end; min-width:0; }
.fb-line > * { min-width:0; }
.fb-l1 { grid-template-columns:4.5rem minmax(0, 1fr) minmax(0, 1fr); }
.fb-l2 { grid-template-columns:minmax(0, 12rem) minmax(0, 1fr); }
.fb-l3 { display:block; }
.fb-bind { display:flex; flex-wrap:wrap; gap:.35rem; }
.fb-bind select.in { flex:1 1 14rem; width:auto; min-width:0; }
.fb-bind .fb-prm { flex:0 1 5.5rem; width:5.5rem; min-width:0; }
.fb-l4 { display:flex; flex-wrap:wrap; align-items:center; gap:.45rem 1rem; }
.fb-l4 .fb-help { flex:1 1 14rem; width:auto; min-width:0; }
.fb-tick { display:inline-flex; align-items:center; gap:.4rem; font-size:.88rem;
  font-weight:600; cursor:pointer; }
.fb-tick input[type=checkbox] { width:24px; height:24px; margin:0; flex:none; }
.fb-key { margin:0; overflow-wrap:anywhere; }
.fb-new .fb-order { align-self:center; }
@media (max-width:599px) {
  .fb-l1 { grid-template-columns:4rem minmax(0, 1fr); }
  .fb-l1 .fb-af { grid-column:1 / -1; }
  .fb-l2 { grid-template-columns:minmax(0, 1fr); }
}

/* Review, 2026-10-14. The pick-list after its form submits: the pinned row
   (the menu and its group row on a tablet or PC, the slim top bar on a
   phone) must not cover the heading the page jumps to. */
#ape-pick { scroll-margin-top:7.5rem; }
@media (max-width:599px) { #ape-pick { scroll-margin-top:5rem; } }
/* A picked age with no table: its results get no points, and the
   neighbouring table is set apart as an aside, never as that age's table. */
.ape-aside { border-left:var(--bar) solid var(--n-300); padding:.2rem 0 .2rem .8rem;
  margin-top:.6rem; color:var(--n-700); }
.ape-aside .eyebrow { margin:0 0 .3rem; }
/* Delete is the far end of the line from Required, in the warning colour. */
.fb-l4 .fb-delete { margin-left:auto; color:var(--red-ink); }

/* ── the tree nav: Rules, Documentation, Admin → How to run it ────────────
   docs/19oct2026.md Part 4 (Paul, 2026-10-19, D4): a left-hand nav like a
   folder tree, clearly indented, compact, collapsible -- one component,
   templates/_tree_nav.html. Folders are <details>, so they work with
   JavaScript off. Below 740 px the tree folds behind a "Contents" button
   above the page: an empty <details> whose next sibling is the tree. */
.withtree { display:grid; grid-template-columns:minmax(0, 1fr); gap:1rem; align-items:start; }
/* Beside the page from 1024 px; on a tablet the tree sits above it, open,
   because a 740 px screen with a 15rem tree left the Rules table too narrow
   to fit without scrolling (tests/test_layout.py, 744 px). */
@media (min-width:1024px) {
  .withtree { grid-template-columns:minmax(11rem, 15rem) minmax(0, 1fr); }
}
.treebody { min-width:0; }
.tree-toggle { display:none; }
.tree { font-size:.84rem; line-height:1.3; min-width:0; }
.tree ul { list-style:none; margin:0; padding:0; }
.tree ul ul { margin:.1rem 0 .3rem .45rem; padding-left:.55rem;
  border-left:1px solid var(--n-300); }
.tree li { margin:0; }
.tree a, .tree summary { display:block; padding:.2rem .35rem; border-radius:var(--r);
  color:var(--ink); text-decoration:none; overflow-wrap:anywhere; }
.tree summary { cursor:pointer; font-weight:700; }
.tree a:hover, .tree summary:hover { background:var(--n-100); }
.tree a[aria-current="page"] { background:var(--tint-gold); font-weight:700;
  box-shadow:inset var(--bar) 0 0 var(--gold); }
@media (max-width:739.98px) {
  .tree-toggle { display:block; }
  .tree-toggle > summary { cursor:pointer; font-weight:700; font-size:.9rem;
    padding:.5rem .7rem; border:1px solid var(--n-300); border-radius:var(--r);
    background:var(--paper); min-height:40px; box-sizing:border-box; }
  .tree-toggle:not([open]) + .tree { display:none; }
  .tree { border:1px solid var(--n-200); border-radius:var(--r); padding:.4rem;
    background:var(--paper); }
  .tree a, .tree summary { padding:.45rem .4rem; }
}

/* A handbook page: our own Markdown, rendered with raw HTML escaped. Long
   lines wrap rather than scroll, at any width. */
.handbook { overflow-wrap:break-word; }
.handbook h1 { margin-bottom:.15rem; }
.handbook h1 + p > em:only-child { color:var(--n-700); font-size:.82rem; }
.handbook h2 { margin-top:1.2rem; }
.handbook p, .handbook li { line-height:1.5; max-width:46rem; }
.handbook pre { white-space:pre-wrap; overflow-wrap:anywhere; background:var(--n-100);
  border:1px solid var(--n-200); border-radius:var(--r); padding:.6rem .7rem; font-size:.85rem; }
.handbook code { overflow-wrap:anywhere; }
.handbook table { margin:.6rem 0; }
.handbook-fallback { border-left:var(--bar) solid var(--gold); background:var(--tint-amber);
  padding:.4rem .6rem; margin-top:0; }

/* How to → Rules → Roles and access (docs/19oct2026.md Part 4, D12): the four
   levels drawn as boxes, and who may see and edit what. Colours are tokens
   only, so the picture swaps with the theme. The SVG scales with its viewBox
   (320 units: about its own size on a 360 px phone) and stops growing at
   28rem, where 11-unit text would otherwise be drawn far too large. */
.roles h2 { margin-top:0; }
.roles-svg { display:block; width:100%; max-width:28rem; height:auto; margin:.2rem auto .8rem;
  font-family:var(--face); }
.roles-svg rect { fill:var(--paper); stroke:var(--n-300); stroke-width:1; }
.roles-svg g[data-role="admin"] rect { fill:var(--tint-gold); stroke:var(--gold); stroke-width:1.5; }
.roles-svg .link { fill:none; stroke:var(--n-500); stroke-width:1.2; }
.roles-svg text { fill:var(--ink); font-size:11px; }
.roles-svg text.title { font-size:13px; font-weight:700; }
.roles-svg text.count { font-size:10px; fill:var(--n-700); }
table.roles-matrix td:first-child { width:34%; }
table.roles-matrix td.lvl-edit { font-weight:700; }
table.roles-matrix td.lvl-none { color:var(--n-500); }
@media (max-width:739.98px) {
  table.cards.roles-matrix td:first-child { width:auto; }
}
