/* Termline design system
 *
 * One stylesheet, no framework, no build step, no content delivery network.
 * A council portal is read on a school laptop over a slow connection in the
 * first week of September; every kilobyte here is one somebody waits for.
 *
 * Four rules run through the whole file:
 *
 *   1. Colour never carries meaning on its own. Every status, every message
 *      and every validation state also has a word and a shape. That is WCAG
 *      1.4.1, and it is also just correct: a printed spend report should not
 *      become ambiguous.
 *   2. Every interactive element has a visible focus ring, and the ring is the
 *      same one everywhere so it is recognisable.
 *   3. Nothing depends on hover. The primary journeys complete on a 375px
 *      touchscreen with one hand.
 *   4. Decoration is derived, never hard-coded. Every tint, glow, gradient and
 *      piece of artwork is mixed from the two brand colours the council sets
 *      in Admin, so re-branding stays a form field.
 *
 * The artwork in static/img is drawn in white at low opacity and always sits
 * over a darkened mix of the brand colour, which is why one file serves both
 * the light and the dark theme and any hue a council chooses.
 */

/* ------------------------------------------------------------- tokens --- */

/* Light is the default, and it is the only palette a reader is given unless
   they ask for the other one.

   The two were never equally finished, and pretending otherwise costs the
   council: the light one is what gets printed for a governors' meeting,
   projected in a hall, and screenshotted into a tender document. A school
   laptop that an IT policy has set to dark should still show the palette the
   council signed off. Dark stays complete behind data-theme="dark" on <html>
   for anyone who wires a control to it. */

:root {
  color-scheme: light;

  /* Brand. Overridden inline by the layout from Admin settings; these two
     are the same values as the shipped default in internal/store/settings.go,
     so a page that somehow loses the inline block still looks like itself. */
  --brand: #1560b0;
  --brand-ink: #0f4c96;

  /* What the primary action darkens to under the cursor.
  
     Mixing towards --ink rather than towards black is what makes one rule
     serve both palettes: in light the ink is near-black and the button gets
     darker, in dark the ink is near-white and it gets lighter, and either way
     it moves away from the label rather than towards it. Mixing towards black
     put dark ink on a dark hover in the dark palette: 1.5:1, an invisible
     label on the most-used button on the site. */
  --brand-hover: color-mix(in srgb, var(--brand) 84%, var(--ink));
  --bad-hover: color-mix(in srgb, var(--bad) 82%, var(--ink));

  /* The decorative set.

     A council brand is one colour and usually a restrained one, which is
     right for a logo and thin for a whole interface: everything tinted from a
     single muted hue reads as one grey wash. So decoration is drawn from four
     hues rotated around the brand in OKLCH, which holds the lightness even
     across them and keeps them recognisably the same family.

     The rotations are chosen, not arbitrary:

       a   the brand itself
       b   −52°, its near neighbour, for cohesion
       c   +168°, its complement, for the one warm spark in a cool palette
       d   +32°, for depth behind a and b rather than beside them

     The default azure gives azure, cyan, amber and indigo: a cool family
     with a single warm accent, which is the arrangement that stays legible
     when a school prints it.

     These are decoration only. Nothing here ever means anything: status,
     validity and outcome are carried by the tokens further down, with a word
     beside them, exactly as before.

     The fallback is the brand and its accent, which is the monochrome wash
     this replaces: a browser without relative colour loses the range and
     keeps the design. */
  --hue-a: var(--brand);
  --hue-b: var(--brand-ink);
  --hue-c: var(--brand);
  --hue-d: var(--brand-ink);

  /* Surfaces and ink. Light is defined on bare :root, so a page always has a
     complete palette before any attribute is considered. The ground is tinted
     rather than grey, which is what makes a white card an object on it rather
     than a slightly different grey. */
  --ground: #f4f7fa;
  --ground: color-mix(in srgb, var(--hue-b) 4%, #f8fafc);
  --surface: #ffffff;
  --surface-2: #eff3f8;
  --surface-2: color-mix(in srgb, var(--hue-a) 6%, #f4f7fa);
  --surface-3: #e2e9f1;
  --surface-3: color-mix(in srgb, var(--hue-a) 11%, #e9eff5);
  /* Ink is not black. It carries a trace of the brand hue, which is what
     stops text on a tinted ground looking like a photocopy laid on coloured
     paper. */
  --ink: #0b1219;
  --ink-2: #434e5b;
  /* Dark enough to clear 4.5:1 against the page ground as well as against a
     card, which #69746e did not: it was 4.47 on --ground. */
  --ink-3: #5d6875;

  /* Hairlines are alpha rather than a flat grey, so one rule reads correctly
     on a card, on the page ground and over a tint. */
  --rule: rgba(11, 26, 46, 0.10);
  --rule-strong: rgba(11, 26, 46, 0.17);
  /* The edge of something you can type into has to clear 3:1 against its own
     background (WCAG 1.4.11), which a hairline does not. */
  --field-line: #737e8a;
  --grid-line: rgba(11, 30, 52, 0.05);
  --grid-line: color-mix(in srgb, var(--hue-a) 13%, transparent);

  --accent-soft: #dde9f7;
  --accent-soft: color-mix(in srgb, var(--hue-a) 14%, var(--surface));
  --accent-ring: rgba(21, 96, 176, 0.32);
  --accent-ring: color-mix(in srgb, var(--hue-a) 42%, transparent);
  --tint: color-mix(in srgb, var(--hue-a) 8%, transparent);

  /* What is legible ON the brand. A light-theme brand is dark enough to carry
     white; a dark-theme brand is a light mint, and white on it is 2.3:1. The
     token is what stops every primary button failing at the second palette. */
  --on-brand: #ffffff;

  /* Where white text sits on colour. Each of the three is mixed towards a
     near-black of its own hue rather than a common one, so the hero is a
     gradient through three deep colours instead of three shades of one. */
  --deep-1: color-mix(in srgb, var(--brand) 52%, #071410);
  --deep-2: color-mix(in srgb, var(--brand) 86%, #071410);
  --deep-3: color-mix(in srgb, var(--brand) 60%, #071410);

  /* Status. Each pairs a strong ink with a soft background; both are used
     together with an icon and a word, never alone.

     These are picked against the primary rather than with it. Information is
     the awkward one: the conventional colour for it is blue, and the brand is
     now blue, so an informational notice would have been indistinguishable
     from a branded one. It moves to indigo (next to the brand, plainly not
     it), and the remaining four sit at even intervals around the wheel so no
     two states are neighbours. */
  --good: #0d7350;
  --good-soft: #d9f2e6;
  --warn: #7d5504;
  --warn-soft: #fbeecd;
  --bad: #9e2b1e;
  --bad-soft: #fce3dd;
  --info: #3a4a9c;
  --info-soft: #e4e7fa;
  --quiet: #4d5a68;
  --quiet-soft: #e9edf2;

  /* Type. A system stack keeps the first paint immediate and avoids shipping
     a webfont the council has not licensed. The variable Segoe and SF faces
     are named first because they are what the reader own operating system
     draws its own interface with, so the page belongs to the machine it is
     being read on. */
  --font: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
    "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --font-display: "SF Pro Display", -apple-system, BlinkMacSystemFont,
    "Segoe UI Variable Display", "Segoe UI", system-ui, Roboto, "Helvetica Neue",
    Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "SF Mono",
    Menlo, Consolas, monospace;

  /* A fluid scale. The bounds are in rem so text-only resizing still works;
     the middle term is what lets a headline behave like a headline on a
     laptop without shouting on a phone. */
  --step--1: clamp(0.82rem, 0.795rem + 0.11vw, 0.875rem);
  --step-0: clamp(1rem, 0.975rem + 0.12vw, 1.0625rem);
  --step-1: clamp(1.125rem, 1.085rem + 0.2vw, 1.25rem);
  --step-2: clamp(1.3rem, 1.22rem + 0.38vw, 1.5rem);
  --step-3: clamp(1.55rem, 1.4rem + 0.7vw, 1.95rem);
  --step-4: clamp(1.95rem, 1.66rem + 1.35vw, 2.7rem);
  --step-5: clamp(2.35rem, 1.85rem + 2.4vw, 3.6rem);

  /* Spacing on a four-pixel grid. */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2rem;
  --s7: 3rem;
  --s8: 4rem;

  --radius: 10px;
  --radius-lg: 16px;
  --radius-xl: 24px;
  --radius-pill: 999px;

  /* One height for everything a person clicks or types into, so a control
     and the button beside it are the same object. */
  --control-h: 3rem;

  /* Depth is two shadows, not one: a tight contact shadow that says the edge
     is real, and a wide soft one that says how far off the page it is. Both
     are tinted rather than neutral, because a grey shadow over a tinted
     ground is the one thing that makes a bright palette look dirty. */
  --shadow-1: 0 1px 2px rgba(10, 24, 42, 0.05), 0 1px 1px rgba(10, 24, 42, 0.03);
  --shadow-2: 0 1px 2px rgba(10, 24, 42, 0.05), 0 10px 26px -14px rgba(8, 30, 60, 0.24);
  --shadow-3: 0 2px 4px rgba(10, 24, 42, 0.06), 0 26px 52px -22px rgba(8, 30, 60, 0.32);
  --shadow-brand: 0 1px 2px rgba(10, 24, 42, 0.12), 0 10px 24px -12px var(--accent-ring);

  /* The masthead becomes sticky once there is height to spare; anything else
     that sticks has to clear it. */
  --header-h: 0px;

  --measure: 68ch;
  --page: 1200px;
}

/* The triad proper, wherever the browser can derive it. */
@supports (color: oklch(from red l c h)) {
  :root {
    --hue-a: oklch(from var(--brand) clamp(0.46, calc(l * 1.34), 0.66) clamp(0.08, calc(c * 1.55), 0.16) h);
    --hue-b: oklch(from var(--brand) clamp(0.5, calc(l * 1.42), 0.7) clamp(0.09, calc(c * 1.6), 0.155) calc(h - 52));
    --hue-c: oklch(from var(--brand) clamp(0.56, calc(l * 1.6), 0.76) clamp(0.09, calc(c * 1.7), 0.16) calc(h + 168));
    --hue-d: oklch(from var(--brand) clamp(0.44, calc(l * 1.28), 0.62) clamp(0.08, calc(c * 1.6), 0.16) calc(h + 32));

    /* The three deep grounds white text sits on, set at an explicit lightness
       rather than mixed down towards black. Mixing in sRGB drove two of them
       out of gamut and the clipping took the hue with it; naming the
       lightness keeps each one in gamut and safely above 6:1.

       The order is indigo, brand, cyan: a sweep across the cool side of the
       set, with the warm complement kept out of it and saved for the small
       accents where one spark of it is worth something. */
    --deep-1: oklch(from var(--hue-d) 0.34 0.1 h);
    --deep-2: oklch(from var(--hue-a) 0.4 0.11 h);
    --deep-3: oklch(from var(--hue-b) 0.37 0.095 h);
  }
}

@media (min-width: 900px) {
  :root { --header-h: 7rem; }
}

/* Dark, for a reader who asks for it by putting data-theme="dark" on <html>.
   It is deliberately not reached from the operating system preference; see
   the note at the top of this section. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --ground: #080b10;
  --surface: #12171d;
  --surface-2: #1a2028;
  --surface-3: #252d37;
  --ink: #e9eef4;
  --ink-2: #aab4c1;
  --ink-3: #7e8896;
  --rule: rgba(232, 240, 252, 0.10);
  --rule-strong: rgba(232, 240, 252, 0.17);
  --field-line: #6c7787;
  --grid-line: color-mix(in srgb, var(--hue-a) 12%, transparent);
  --brand: #5aaef0;
  --brand-ink: #8ecbf7;
  --on-brand: #05101c;
  --accent-soft: color-mix(in srgb, var(--hue-a) 18%, var(--surface));
  --accent-ring: color-mix(in srgb, var(--hue-a) 46%, transparent);
  --tint: color-mix(in srgb, var(--hue-a) 11%, transparent);
  --deep-1: color-mix(in srgb, var(--brand) 34%, #05100c);
  --deep-2: color-mix(in srgb, var(--brand) 58%, #05100c);
  --deep-3: color-mix(in srgb, var(--brand) 40%, #05100c);
  --good: #63cf9d;
  --good-soft: #10302a;
  --warn: #e0b45c;
  --warn-soft: #2d2412;
  --bad: #ec9782;
  --bad-soft: #331c18;
  --info: #a3a9f0;
  --info-soft: #1d1f3d;
  --quiet: #98a3b1;
  --quiet-soft: #1f262e;
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-2: 0 1px 2px rgba(0, 0, 0, 0.45), 0 10px 26px -14px rgba(0, 0, 0, 0.85);
  --shadow-3: 0 2px 4px rgba(0, 0, 0, 0.5), 0 26px 52px -22px rgba(0, 0, 0, 0.95);
  --shadow-brand: 0 1px 2px rgba(0, 0, 0, 0.5), 0 10px 24px -12px var(--accent-ring);
}

@supports (color: oklch(from red l c h)) {
  :root[data-theme="dark"] {
    --deep-1: oklch(from var(--hue-d) 0.27 0.09 h);
    --deep-2: oklch(from var(--hue-a) 0.31 0.095 h);
    --deep-3: oklch(from var(--hue-b) 0.29 0.085 h);
  }
}

/* --------------------------------------------------------------- base --- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* [hidden] means hidden.
   The attribute is a display: none in the user-agent stylesheet, and any
   author rule that sets display beats it. .btn is inline-flex and .hint is
   block, so anything shipped hidden and revealed by a script was drawn for
   everybody the script never reached, which is the exact opposite of what
   shipping it hidden was for.

   It had been true of the map's own controls since the map shipped: "Put the
   pin in the middle" and "Remove the pin" were visible, and useless, to a
   reader with scripting off. Nothing failed, nothing was logged, and the
   documentation said the opposite. */
[hidden] {
  display: none !important;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* An in-page link must not land underneath the sticky masthead. */
  scroll-padding-top: calc(var(--header-h) + var(--s4));
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--step-0);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-synthesis-weight: none;
}

/* The page is never a flat slab of grey. Two fixed layers sit behind
   everything: a wash mixed from the council's own brand colour, and a faint
   ruled grid that fades out before it reaches the content it would compete
   with. Both are drawn rather than downloaded, so they cost no request and
   re-brand with the colour picker. */
body::before,
body::after {
  content: "";
  position: fixed;
  z-index: -1;
  pointer-events: none;
}
/* Inset past the viewport on every side, so the layer can drift and scale
   without ever bringing an edge into view. */
body::before {
  inset: -12%;
  animation: wash-drift 78s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
  background:
    radial-gradient(56rem 32rem at 2% -12%, color-mix(in srgb, var(--hue-a) 26%, transparent), transparent 66%),
    radial-gradient(48rem 30rem at 99% -6%, color-mix(in srgb, var(--hue-b) 24%, transparent), transparent 62%),
    radial-gradient(34rem 22rem at 82% 38%, color-mix(in srgb, var(--hue-c) 13%, transparent), transparent 66%),
    radial-gradient(40rem 26rem at 12% 62%, color-mix(in srgb, var(--hue-d) 14%, transparent), transparent 66%),
    radial-gradient(66rem 44rem at 40% 108%, color-mix(in srgb, var(--hue-b) 16%, transparent), transparent 70%);
}
body::after {
  inset: 0;
  /* The grid itself never moves: a ruled surface that slides reads as a
     rendering fault. Only how strongly it shows changes, and only by opacity,
     which the compositor can take on its own. This layer covers the whole
     viewport, so it is the last place to animate anything that repaints. */
  animation: grid-breathe 46s ease-in-out infinite alternate;
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
  background-size: 68px 68px;
  -webkit-mask-image: radial-gradient(130% 86% at 50% 0%, #000 6%, transparent 68%);
  mask-image: radial-gradient(130% 86% at 50% 0%, #000 6%, transparent 68%);
}

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--s3);
  font-family: var(--font-display);
  line-height: 1.2;
  letter-spacing: -0.016em;
  font-weight: 640;
  text-wrap: balance;
}

h1 { font-size: var(--step-4); letter-spacing: -0.028em; font-weight: 680; }
h2 { font-size: var(--step-3); letter-spacing: -0.022em; }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
h5, h6 { font-size: var(--step-0); }

/* Headings that name a card or a panel rather than a page. These three are
   the sizes the templates actually reach for, and having them as classes is
   what keeps the markup free of inline style attributes, which the content
   security policy discards anyway. */
.h-lg { font-size: var(--step-2); letter-spacing: -0.018em; }
.h-sub { font-size: var(--step-1); letter-spacing: -0.012em; }
.h-min { font-size: var(--step-0); letter-spacing: 0; font-weight: 660; }

p { margin: 0 0 var(--s4); }
p:last-child { margin-bottom: 0; }

/* figure and blockquote carry a browser default of 40px down each side, which
   is invisible until one of them is a grid item, and then it is a card 80px
   narrower than its column, sized to look like a mistake. Reset here rather
   than on each component, so the next figure does not have to remember. */
figure, blockquote { margin: 0; }

a {
  color: var(--brand-ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  transition: text-decoration-color 0.12s ease, color 0.12s ease;
}
a:hover { text-decoration-thickness: 2px; text-decoration-color: currentColor; }

/* One focus ring, used everywhere, always visible. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
  border-radius: 3px;
}

img, svg, video { max-width: 100%; height: auto; }

hr {
  border: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--rule-strong) 18%, var(--rule-strong) 82%, transparent);
  margin: var(--s6) 0;
}

code, kbd, samp {
  font-family: var(--font-mono);
  font-size: 0.88em;
}

small { font-size: var(--step--1); }

::selection { background: var(--accent-soft); color: var(--ink); }

/* Skip link: the first thing in the tab order on every page. */
.skip {
  position: absolute;
  left: var(--s4);
  top: -100%;
  z-index: 200;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  color: var(--ink);
  border: 2px solid var(--brand);
  border-radius: var(--radius);
  box-shadow: var(--shadow-3);
  font-weight: 600;
  text-decoration: none;
}
.skip:focus { top: var(--s3); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------------------------------------------------------------- layout --- */

/* overflow-x: clip rather than hidden, deliberately. A full-bleed banner is
   half a scrollbar wider than this container on a page that scrolls, and clip
   removes that overflow without making .shell a scroll container, so a wide
   table inside it still gets its own horizontal scroller, and position: sticky
   still works, neither of which survives overflow: hidden. */
.shell { flex: 1 0 auto; overflow-x: clip; }

.wrap {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding-inline: var(--s4);
}
@media (min-width: 720px) {
  .wrap { padding-inline: var(--s5); }
}

.prose { max-width: var(--measure); }

.stack > * + * { margin-top: var(--s4); }
.stack-lg > * + * { margin-top: var(--s6); }

.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
}
.row-between { justify-content: space-between; }
.row:has(> .field) { align-items: flex-end; }

/* A control told to grow must not also be told to be the full width of its
   row, or it fills the row and pushes the button beside it onto a line of its
   own, which is what happened to every search box on the site. */
input.grow, select.grow { width: auto; flex: 1 1 12rem; min-width: 0; }
.grow { flex: 1 1 auto; min-width: 0; }

.grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 262px), 1fr));
}
.grid-2 { display: grid; gap: var(--s4); grid-template-columns: 1fr; }
.grid-3 { display: grid; gap: var(--s4); grid-template-columns: 1fr; }
@media (min-width: 760px) {
  .grid-2 { grid-template-columns: repeat(2, 1fr); }
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
}

.sidebar-layout {
  display: grid;
  gap: var(--s5);
  grid-template-columns: 1fr;
  align-items: start;
}
@media (min-width: 940px) {
  .sidebar-layout { grid-template-columns: 250px 1fr; gap: var(--s6); }
  .sidebar-layout.aside-right { grid-template-columns: 1fr 340px; }
}

/* ------------------------------------------------------------- header --- */

/* Frosted rather than opaque: the wash and the grid behind the page carry on
   underneath it, which is what stops a sticky bar reading as a lid. */
.masthead {
  position: relative;
  z-index: 100;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  border-bottom: 1px solid var(--rule);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .masthead { background: var(--surface); }
}
@media (min-width: 900px) {
  .masthead { position: sticky; top: 0; }
}

.masthead-top {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding-block: var(--s3);
  flex-wrap: wrap;
}

.brandmark {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  text-decoration: none;
  color: inherit;
  padding-block: var(--s1);
}
.brandmark:hover { text-decoration: none; }
.brandmark .mark {
  width: 38px; height: 38px;
  flex: none;
  border-radius: 11px;
  background: linear-gradient(145deg, var(--deep-2) 0%, var(--deep-1) 62%, var(--deep-3) 100%);
  color: #fff;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.92rem;
  letter-spacing: -0.03em;
  box-shadow: var(--shadow-brand);
}
.brandmark img.mark { object-fit: contain; background: transparent; box-shadow: none; }
.brandmark .names { display: flex; flex-direction: column; line-height: 1.2; }
.brandmark .council {
  font-size: 0.68rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 640;
}
.brandmark .site {
  font-family: var(--font-display);
  font-weight: 660;
  font-size: 1.06rem;
  letter-spacing: -0.018em;
}

.masthead-actions { margin-left: auto; display: flex; align-items: center; gap: var(--s2); }

/* Primary navigation. On a narrow screen it becomes a horizontally
   scrollable strip rather than a hamburger, because a menu you have to open
   is a menu an occasional user does not find. */
.mainnav {
  border-top: 1px solid var(--rule);
}
.mainnav ul {
  position: relative;
  display: flex;
  gap: var(--s1);
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-x: auto;
  scrollbar-width: thin;
}
.mainnav a {
  position: relative;
  display: block;
  padding: var(--s3) var(--s3) calc(var(--s3) - 1px);
  color: var(--ink-2);
  text-decoration: none;
  font-weight: 560;
  white-space: nowrap;
  border-radius: var(--radius) var(--radius) 0 0;
}
.mainnav a::after {
  content: "";
  position: absolute;
  left: var(--s3); right: var(--s3);
  bottom: 0;
  height: 3px;
  border-radius: 3px 3px 0 0;
  background: transparent;
}
.mainnav a:hover { color: var(--ink); background: var(--tint); }
.mainnav a[aria-current="page"] { color: var(--brand-ink); font-weight: 620; }
.mainnav a[aria-current="page"]::after {
  background: linear-gradient(90deg, var(--hue-a), var(--hue-b));
}

/* Acting-on-behalf-of banner. It is deliberately loud: somebody about to
   commit another organisation's money must not be in any doubt whose. */
.acting-banner {
  background: var(--warn-soft);
  border-bottom: 2px solid var(--warn);
  color: var(--ink);
}
.acting-banner .wrap {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
  padding-block: var(--s3);
  font-size: var(--step--1);
}
.acting-banner strong { color: var(--warn); }

/* --------------------------------------------------------------- page --- */

/* No glow behind the heading.
 *
 * There was one, and it could not be made to work: the page head is as wide
 * as the content column, so a radial gradient inside it is cut off square at
 * both sides and reads as a pale panel with two hard vertical seams rather
 * than as light. Bleeding it past the column instead trades the seams for a
 * horizontal scrollbar the width of the bleed.
 *
 * The fixed wash on the body already lights the top of every page, and it is
 * the full width of the viewport, so it has no edges to get wrong. */
.page-head {
  padding-block: var(--s7) var(--s5);
}
.eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 660;
  color: var(--brand-ink);
  margin: 0 0 var(--s2);
}
.eyebrow-quiet { color: var(--ink-3); }
/* A short rule before the eyebrow, so the top of a page starts with colour
   rather than with small grey capitals. */
.page-head .eyebrow, .hero .eyebrow, .auth-form .eyebrow {
  display: flex;
  align-items: center;
  gap: 0.6em;
}
.page-head .eyebrow::before, .hero .eyebrow::before, .auth-form .eyebrow::before {
  content: "";
  width: 1.8rem;
  height: 3px;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--hue-a), var(--hue-b));
  flex: none;
}
.hero .eyebrow::before { background: rgba(255, 255, 255, 0.7); }
.page-head .lede {
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--ink-2);
  max-width: var(--measure);
  margin: var(--s3) 0 0;
}
.page-head .row { margin-top: var(--s5); }

.breadcrumbs {
  padding-block: var(--s4) 0;
  font-size: var(--step--1);
}
.breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  list-style: none;
  margin: 0;
  padding: 0;
  color: var(--ink-3);
}
.breadcrumbs li {
  display: flex;
  align-items: center;
}
.breadcrumbs li + li::before {
  content: "/";
  margin-right: var(--s2);
  color: var(--rule-strong);
}
/* The last crumb is the page you are on, so it is a span and not a link, and
   that is why it sat three pixels above its neighbours. Standalone links get
   padding to make a comfortable touch target, a span gets none, and the
   difference is exactly the padding. It is not a target, but it has to be the
   same box as the ones beside it. */
.breadcrumbs a,
.breadcrumbs [aria-current="page"] {
  display: inline-block;
  padding-block: 0.2rem;
  min-height: 1.5rem;
}
.breadcrumbs a { color: var(--ink-2); text-decoration: none; }
.breadcrumbs a:hover { text-decoration: underline; }

.section { padding-block: 0 var(--s7); }
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s5);
}
.section-head h2 { margin: 0; font-size: var(--step-2); }
.section-head p { margin: 0; color: var(--ink-2); font-size: var(--step--1); }

/* ---------------------------------------------------------------- hero --- */

/* The one place on the site that is allowed to be loud. A darkened mix of the
   brand carries the artwork in static/img/hero.svg, which is drawn in white
   at low opacity so it survives any hue and either theme. */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin-block: var(--s5) var(--s6);
  padding: clamp(var(--s6), 5vw, var(--s8)) clamp(var(--s5), 5vw, var(--s8));
  border-radius: var(--radius-xl);
  background-color: var(--brand);
  /* The fallback darkens whatever hue the council chose by enough that white
     is still safe on it, for a browser too old to parse the mix below. */
  background-image: linear-gradient(128deg, rgba(4, 16, 12, 0.74) 0%, rgba(4, 16, 12, 0.56) 46%, rgba(4, 16, 12, 0.7) 100%);
  background-image: linear-gradient(128deg, var(--deep-1) 0%, var(--deep-2) 46%, var(--deep-3) 100%);
  color: #fff;
  box-shadow: var(--shadow-3);
}
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: url("/static/img/hero.svg") center / cover no-repeat;
}
/* A fine inner highlight along the top edge, the way light catches a raised
   surface. It is one pixel and it is most of the reason the panel reads as an
   object rather than a coloured rectangle. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22),
              inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
/* Everything in the panel is lifted above the decoration, except the
   decoration itself. This rule enumerates what is decorative, so a new
   decorative child has to be named here, and one that is not gets
   position: relative at a higher specificity than its own rule, which turns an
   absolutely positioned backdrop into an 810px-tall image in the flow. That is
   how the banner came to be twice the height of the window. */
.hero > *:not(.aurora, .rings, .hero-backdrop) { position: relative; }
.hero .eyebrow { color: rgba(255, 255, 255, 0.78); }
.hero h1 {
  margin: 0;
  font-size: var(--step-5);
  letter-spacing: -0.032em;
  max-width: 18ch;
}
.hero .lede {
  margin: var(--s4) 0 0;
  font-size: var(--step-1);
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.86);
  max-width: 52ch;
}
.hero .btn-secondary {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.34);
  color: #fff;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.hero .btn-secondary:hover { background: rgba(255, 255, 255, 0.2); }
.hero .btn-light {
  background: #fff;
  color: var(--deep-2);
  border-color: transparent;
}
.hero .btn-light:hover { background: rgba(255, 255, 255, 0.9); }
.hero a { color: #fff; }
.hero :where(a, button, input, select):focus-visible { outline-color: #fff; }

/* Facts under a hero: three or four short claims, hairline-separated. */
.hero-facts {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.25rem), 1fr));
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}
.hero-facts > div { margin: 0; }
.hero-facts .value {
  display: block;
  margin: var(--s1) 0 0;
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 690;
  letter-spacing: -0.028em;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
}
.hero-facts .label {
  display: block;
  font-size: var(--step--1);
  font-weight: 500;
  color: rgba(255, 255, 255, 0.74);
}

/* The hero search sits on colour, so it gets a solid field rather than a
   transparent one: a search box you cannot see is not a search box. */
.hero-search {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: stretch;
  margin-top: var(--s6);
  max-width: 40rem;
}
.hero-search input[type="search"] { width: auto; flex: 1 1 12rem; min-width: 0; }
.hero-search input[type="search"] {
  background: rgba(255, 255, 255, 0.96);
  border-color: transparent;
  color: #10201a;
  box-shadow: var(--shadow-2);
}
.hero-search input[type="search"]::placeholder { color: #5b6b64; }
.hero-search input[type="search"]:focus {
  background: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.55);
}

/* ------------------------------------------------------------ buttons --- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  padding: 0.6rem 1.15rem;
  min-height: var(--control-h);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background-color: var(--brand);
  background-image: linear-gradient(180deg, color-mix(in srgb, #fff 12%, transparent), transparent 62%);
  color: var(--on-brand);
  font: inherit;
  font-weight: 600;
  letter-spacing: -0.005em;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  box-shadow: var(--shadow-brand);
  transition: background-color 0.16s ease, border-color 0.16s ease,
              box-shadow 0.16s ease, transform 0.12s cubic-bezier(0.22, 0.8, 0.28, 1);
}
.btn:hover {
  background-color: var(--brand-hover);
  text-decoration: none;
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(10, 24, 42, 0.1), 0 12px 26px -12px var(--accent-ring);
}
.btn:active { transform: translateY(1px); box-shadow: var(--shadow-1); }
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.btn-secondary {
  background-color: var(--surface);
  background-image: none;
  color: var(--ink);
  border-color: var(--field-line);
  box-shadow: var(--shadow-1);
}
.btn-secondary:hover { background-color: var(--surface-2); border-color: var(--field-line); }

.btn-quiet {
  background: transparent;
  background-image: none;
  color: var(--brand-ink);
  border-color: transparent;
  padding-inline: var(--s3);
  box-shadow: none;
}
.btn-quiet:hover { background: var(--tint); }

.btn-danger { background-color: var(--bad); color: var(--on-brand); box-shadow: var(--shadow-1); }
.btn-danger:hover { background-color: var(--bad-hover); }
.btn-sm { min-height: 2.25rem; padding: 0.3rem 0.7rem; font-size: var(--step--1); border-radius: 8px; }
.btn-lg { min-height: 3.25rem; padding: 0.9rem 1.6rem; font-size: var(--step-1); border-radius: 12px; }
.btn-block { width: 100%; }

.btn-group { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; }

/* A link that behaves as a button but reads as a link, used for destructive
   secondary actions where a heavy button would be misleading. */
.linkbtn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--brand-ink);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  cursor: pointer;
}
.linkbtn.danger { color: var(--bad); }

/* -------------------------------------------------------------- cards --- */

.card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  box-shadow: var(--shadow-2);
}
.card-tight { padding: var(--s4); }
.card-foot {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
}
/* A card that carries the brand rather than sitting quietly under it. */
.card-feature {
  overflow: hidden;
  background:
    radial-gradient(34rem 18rem at 100% 0%, color-mix(in srgb, var(--hue-b) 20%, transparent), transparent 64%),
    radial-gradient(30rem 16rem at 6% 100%, color-mix(in srgb, var(--hue-a) 16%, transparent), transparent 66%),
    var(--surface);
  border-color: color-mix(in srgb, var(--hue-a) 28%, var(--rule));
}
/* One drifting blob inside the card, so the last thing on a signed-out page
   is not the only still thing on it. */
.card-feature::after {
  content: "";
  position: absolute;
  z-index: 0;
  inset: -50% -30% auto auto;
  width: 26rem;
  height: 26rem;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--hue-d) 26%, transparent), transparent 70%);
  animation: aurora-b 39s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
}
.card-feature > * { position: relative; z-index: 1; }

/* A card that is a link to something, used for catalogue items and courses.
   The arrow is the affordance; the lift on hover is the reward. */
.linkcard {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  text-decoration: none;
  color: inherit;
  height: 100%;
  box-shadow: var(--shadow-1);
  transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
/* The arrow is an affordance, so only a card that is itself a link gets one,
   and only that card reserves the room for it. The catalogue draws the same
   component as a div holding several links, and an arrow there would promise
   a click target that is not one. */
a.linkcard { padding-right: calc(var(--s5) + var(--s4)); }
/* A soft corner glow, mixed from the brand, so a wall of cards has light in
   it rather than being a grid of white boxes. */
.linkcard { --accent: var(--hue-a); }
.linkcard:nth-child(4n + 2) { --accent: var(--hue-b); }
.linkcard:nth-child(4n + 3) { --accent: var(--hue-d); }
.linkcard:nth-child(4n + 4) { --accent: var(--hue-c); }
.linkcard::before {
  content: "";
  position: absolute;
  inset: -42% -32% auto auto;
  width: 19rem; height: 19rem;
  border-radius: 50%;
  background: radial-gradient(circle, color-mix(in srgb, var(--accent) 22%, transparent), transparent 66%);
  opacity: 0.55;
  transition: opacity 0.2s ease;
  pointer-events: none;
}
a.linkcard::after {
  content: "→";
  position: absolute;
  top: var(--s5);
  right: var(--s5);
  color: var(--brand-ink);
  font-size: 1rem;
  opacity: 0.5;
  transition: transform 0.16s ease, opacity 0.16s ease;
}
.linkcard:hover {
  border-color: color-mix(in srgb, var(--accent) 46%, var(--rule));
  box-shadow: var(--shadow-2);
  text-decoration: none;
}
a.linkcard:hover { box-shadow: var(--shadow-3); transform: translateY(-2px); }
.linkcard:hover::before { opacity: 1; }
a.linkcard:hover::after { transform: translateX(3px); opacity: 1; }
a.linkcard:focus-visible { transform: translateY(-2px); }
.linkcard h3 {
  margin: 0;
  font-size: var(--step-1);
  letter-spacing: -0.014em;
  color: var(--brand-ink);
}
.linkcard p { margin: 0; color: var(--ink-2); font-size: var(--step--1); }
.linkcard .meta { margin-top: auto; padding-top: var(--s2); }

/* ------------------------------------------------------------ pictures --- */

/* A picture at the head of a card, bled to its edges.
 *
 * The negative margin has to match the padding it is cancelling, and a link
 * card carries extra padding on the right to leave room for its arrow, so
 * that side gets its own figure rather than one that nearly works.
 *
 * Every one of these is decorative and carries alt="": the title it sits above
 * says the same thing, and a screen reader that read both would say everything
 * twice. */
.card-media {
  display: block;
  margin: calc(var(--s5) * -1) calc(var(--s5) * -1) var(--s4);
  aspect-ratio: 16 / 9;
  width: calc(100% + var(--s5) * 2);
  object-fit: cover;
  background: var(--surface-2);
}
a.linkcard .card-media {
  margin-right: calc((var(--s5) + var(--s4)) * -1);
  width: calc(100% + var(--s5) * 2 + var(--s4));
}
.card-tight .card-media { margin: calc(var(--s4) * -1) calc(var(--s4) * -1) var(--s3); }

/* The picture at the top of an article or a service page. Wider than the
   measure the prose is set to, because it is the one thing on the page that
   should not be column-width. */
.hero-media {
  display: block;
  width: 100%;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-2);
  margin-bottom: var(--s5);
}
@media (max-width: 48rem) {
  .hero-media { aspect-ratio: 16 / 9; }
}

/* A picture step, where the picture is the content rather than decoration.
   .hero-media crops to a 21:9 strip, which is right for a scene at the top of
   an article and wrong for anything that carries information: a phishing email
   with its tells marked loses a quarter of itself and the marks with it. So a
   step picture keeps its own proportions and is shown whole.

   A descendant selector rather than a second .hero-media rule, which
   TestNoPictureClassIsDefinedTwice would refuse and rightly: this refines the
   one declaration, it does not compete with it. */
.step-picture .hero-media {
  aspect-ratio: auto;
  height: auto;
  object-fit: contain;
  background: var(--surface);
}
.step-picture .media-scene-glow { object-fit: cover; }
.step-picture figcaption { margin-top: var(--s2); }

/* The picture beside a service, in its sidebar. */
.aside-media {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--surface-2);
  margin-bottom: var(--s4);
}

/* What the management form shows of the picture a service already has. */
.image-field { display: flex; gap: var(--s4); align-items: flex-start; flex-wrap: wrap; }
.image-field .preview {
  width: 12rem;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--surface-2);
  flex: none;
}
.image-field .controls { flex: 1 1 16rem; min-width: 0; }

/* -------------------------------------------------------- stat tiles --- */

.stats {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}
.stat {
  position: relative;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--s5) var(--s4) var(--s4);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  box-shadow: var(--shadow-1);
}
/* A brand rule along the top edge: the tile is a reading off a dial, and the
   dial belongs to the council. */
.stat::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 22%, transparent));
}
/* Position picks the hue. Nothing is being said by it: the tile carries its
   own label, and the palette rule that colour never means anything on its own
   is exactly why it is safe to use colour freely here. */
.stat { --accent: var(--hue-a); }
.stat:nth-child(4n + 2) { --accent: var(--hue-b); }
.stat:nth-child(4n + 3) { --accent: var(--hue-d); }
.stat:nth-child(4n + 4) { --accent: var(--hue-c); }
.stat .label {
  font-size: var(--step--1);
  color: var(--ink-2);
  font-weight: 560;
}
.stat .value {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 700;
  line-height: 1.08;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.028em;
}
.stat .note { font-size: var(--step--1); color: var(--ink-3); }
.stat a { text-decoration: none; }
.stat a:hover { text-decoration: underline; }

/* --------------------------------------------------------- status pill --- */
/* Every pill carries a word. The dot is a second signal, not the only one. */

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.2em 0.65em;
  border-radius: var(--radius-pill);
  font-size: 0.78rem;
  font-weight: 640;
  line-height: 1.5;
  white-space: nowrap;
  background: var(--quiet-soft);
  color: var(--quiet);
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
}
.pill::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.pill-good { background: var(--good-soft); color: var(--good); }
.pill-warn { background: var(--warn-soft); color: var(--warn); }
.pill-bad { background: var(--bad-soft); color: var(--bad); }
.pill-info { background: var(--info-soft); color: var(--info); }
.pill-brand { background: var(--accent-soft); color: var(--brand-ink); }
.tag {
  display: inline-block;
  padding: 0.14em 0.55em;
  border: 1px solid var(--rule-strong);
  border-radius: 6px;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--ink-2);
  background: color-mix(in srgb, var(--hue-a) 9%, var(--surface));
  border-color: color-mix(in srgb, var(--hue-a) 34%, var(--rule-strong));
  white-space: nowrap;
}

.meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  font-size: var(--step--1);
  color: var(--ink-3);
}
.meta > * { display: inline-flex; align-items: center; gap: 0.35em; }

/* ------------------------------------------------------------ notices --- */
/* A notice always has a label word as well as a colour and a rule. */

.notice {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-radius: var(--radius-lg);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--quiet);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}
.notice .icon {
  display: grid;
  place-items: center;
  width: 1.5rem; height: 1.5rem;
  margin-top: 1px;
  border-radius: 50%;
  background: color-mix(in srgb, currentColor 14%, transparent);
  font-size: 0.85rem;
  font-weight: 700;
  line-height: 1;
}
.notice h3, .notice .notice-title {
  margin: 0 0 var(--s1);
  font-size: var(--step-0);
  font-weight: 660;
}
.notice p { margin: 0 0 var(--s2); font-size: var(--step--1); color: var(--ink-2); }
.notice p:last-child { margin-bottom: 0; }
.notice .actions { margin-top: var(--s3); }

.notice-good { border-left-color: var(--good); background: var(--good-soft); color: var(--good); }
.notice-warn { border-left-color: var(--warn); background: var(--warn-soft); color: var(--warn); }
.notice-bad { border-left-color: var(--bad); background: var(--bad-soft); color: var(--bad); }
.notice-info { border-left-color: var(--info); background: var(--info-soft); color: var(--info); }
.notice-brand { border-left-color: var(--brand); background: var(--accent-soft); color: var(--brand-ink); }
/* The tint belongs to the rule and the icon; the words stay at full contrast. */
.notice-good > div, .notice-warn > div, .notice-bad > div,
.notice-info > div, .notice-brand > div { color: var(--ink); }

/* Flash messages sit at the top of the page after a redirect and take focus,
   so a screen reader announces the outcome of what just happened. */
.flashes { padding-top: var(--s4); }
.flashes .notice { box-shadow: var(--shadow-2); }

/* ------------------------------------------------------------- forms --- */

/* Every field is a column, and the control is pushed to the bottom of it.
   That is what makes a row of fields line up on their boxes when one has a
   hint and its neighbour does not, or when a label wraps onto a second line
   and the one beside it does not. Without it, a two-column form is a staircase. */
.field {
  display: flex;
  flex-direction: column;
  margin-bottom: var(--s5);
}
.field:last-child { margin-bottom: 0; }
.field > input,
.field > select,
.field > textarea,
.field > .money-input,
.field > [data-editor],
/* A filter bar ends in a field holding nothing but the Apply button, and it
   has no label to make it as tall as its neighbours. Without this the button
   sits at the top of its cell while every control beside it sits at the
   bottom. */
.field > button,
.field > .btn { margin-top: auto; }

/* Inside a grid the gap does the spacing, and the field must not also carry a
   margin of its own. This is not tidiness: a grid item stretches to the row
   height less its own margins, so a field with a bottom margin ends up 24px
   shorter than the one beside it that :last-child stripped the margin from,
   and every two-column form on the site was a staircase because of it. */
.grid > .field,
.grid-2 > .field,
.grid-3 > .field,
.answer-row > .field,
.filters .fields > .field { margin-bottom: 0; }

label, .label {
  display: block;
  font-weight: 620;
  margin-bottom: var(--s1);
  letter-spacing: -0.006em;
}
.hint {
  display: block;
  color: var(--ink-2);
  font-size: var(--step--1);
  line-height: 1.5;
  margin-bottom: var(--s2);
}
.optional { color: var(--ink-3); font-weight: 500; }

input[type="text"], input[type="email"], input[type="password"], input[type="search"],
input[type="tel"], input[type="url"], input[type="number"], input[type="date"],
input[type="time"], input[type="datetime-local"], select, textarea {
  display: block;
  width: 100%;
  padding: 0.55rem 0.8rem;
  font: inherit;
  line-height: 1.4;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--field-line);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 2px rgba(10, 24, 42, 0.04);
  transition: border-color 0.14s ease, box-shadow 0.14s ease, background-color 0.14s ease;
}

/* One explicit height rather than a minimum, because a date input carries an
   internal picker button that makes it two pixels taller than the text field
   beside it, which is invisible on its own and obvious in a row of four. */
input[type="text"], input[type="email"], input[type="password"], input[type="search"],
input[type="tel"], input[type="url"], input[type="number"], input[type="date"],
input[type="time"], input[type="datetime-local"], select {
  height: var(--control-h);
}
textarea { min-height: 8rem; resize: vertical; line-height: 1.55; }

/* The chevron is drawn once, as a data URI, so the select carries no request
   and no second file to keep in step with the palette. */
select {
  appearance: none;
  padding-right: 2.4rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1.5 1.75 6 6.25l4.5-4.5' fill='none' stroke='%23596862' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.85rem center;
  background-size: 12px 8px;
}
select[multiple] { background-image: none; padding-right: 0.8rem; }

input:focus, select:focus, textarea:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--accent-ring);
  outline: none;
}
input:hover:not(:focus), select:hover:not(:focus), textarea:hover:not(:focus) {
  border-color: color-mix(in srgb, var(--field-line) 70%, var(--ink));
}
input::placeholder, textarea::placeholder { color: var(--ink-3); opacity: 1; }
input[readonly], input[disabled], select[disabled], textarea[disabled] {
  background: var(--surface-2);
  color: var(--ink-2);
  border-color: var(--rule-strong);
  box-shadow: none;
  cursor: not-allowed;
}

.input-narrow { max-width: 12rem; }
.input-medium { max-width: 22rem; }

/* Money inputs get a visible prefix rather than a placeholder, so the unit is
   still there once something is typed. */
.money-input { display: flex; align-items: stretch; max-width: 14rem; }
.money-input .prefix {
  display: grid;
  place-items: center;
  padding-inline: 0.8rem;
  border: 1px solid var(--field-line);
  border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--surface-2);
  color: var(--ink-2);
  font-weight: 620;
}
.money-input input {
  border-radius: 0 var(--radius) var(--radius) 0;
  font-variant-numeric: tabular-nums;
}

fieldset {
  border: 0;
  padding: 0;
  margin: 0 0 var(--s5);
  min-width: 0;
}
legend {
  padding: 0;
  font-family: var(--font-display);
  font-weight: 640;
  font-size: var(--step-1);
  letter-spacing: -0.012em;
  margin-bottom: var(--s2);
}

/* Validation. The message sits above the control, is announced, and the
   control carries a thick red edge as well as the text. */
.field-error input, .field-error select, .field-error textarea {
  border-color: var(--bad);
  border-left-width: 4px;
}
.error-text {
  display: flex;
  gap: 0.4em;
  align-items: flex-start;
  color: var(--bad);
  font-weight: 600;
  font-size: var(--step--1);
  margin-bottom: var(--s2);
}
.error-summary {
  border: 1px solid var(--bad);
  border-left: 5px solid var(--bad);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  margin-bottom: var(--s5);
  background: var(--surface);
  box-shadow: var(--shadow-2);
}
.error-summary h2 { margin: 0 0 var(--s2); font-size: var(--step-1); color: var(--bad); }
.error-summary ul { margin: 0; padding-left: var(--s5); }
.error-summary a { color: var(--bad); font-weight: 600; }

/* Checkboxes and radios large enough to hit with a thumb. */
.choice {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3);
  align-items: start;
  padding: var(--s3) 0;
  cursor: pointer;
}
.choice input[type="checkbox"], .choice input[type="radio"] {
  width: 22px; height: 22px;
  margin: 0.15rem 0 0;
  accent-color: var(--brand);
  flex: none;
  cursor: pointer;
}
.choice .choice-label { font-weight: 600; display: block; }
.choice .choice-hint { display: block; color: var(--ink-2); font-size: var(--step--1); margin-top: 2px; }

/* A long list of choices (every colleague at a school, say) in as many
   columns as there is room for. auto-fill with a minimum is the right tool
   rather than a media query: it responds to the width this grid actually has,
   so it behaves the same inside a narrow card on a wide page as it does in a
   narrow window, which is the mistake a media query cannot help making. */
.choicegrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: 0 var(--s5);
}

/* A choice presented as a selectable card, used where the options carry real
   consequences and deserve room to explain themselves. */
.choice-card {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s3);
  align-items: start;
  padding: var(--s4);
  border: 1px solid var(--field-line);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  margin-bottom: var(--s3);
  transition: border-color 0.14s ease, background-color 0.14s ease, box-shadow 0.14s ease;
}
.choice-card input[type="radio"], .choice-card input[type="checkbox"] {
  width: 22px; height: 22px;
  margin: 0.1rem 0 0;
  accent-color: var(--brand);
  flex: none;
}
.choice-card:hover { border-color: var(--brand); }
.choice-card:has(input:checked) {
  border-color: var(--brand);
  background: var(--accent-soft);
  box-shadow: var(--shadow-brand);
}
.choice-card:has(input:focus-visible) {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
}

/* The two grants of IAM-3 are rendered as switches. Each switch says what it
   permits, in a sentence, because "Finance rights" is exactly the label that
   makes people grant too much. */
.switch-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s4);
  align-items: center;
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--rule);
}
.switch-row:last-of-type { border-bottom: 0; }
.switch-row .switch-text .name { font-weight: 640; display: block; }
.switch-row .switch-text .what { color: var(--ink-2); font-size: var(--step--1); display: block; margin-top: 2px; }

.switch { position: relative; display: inline-flex; align-items: center; gap: var(--s2); cursor: pointer; }
.switch input {
  position: absolute;
  opacity: 0;
  width: 52px; height: 32px;
  margin: 0;
  cursor: pointer;
}
.switch .track {
  width: 52px; height: 32px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  border: 1px solid var(--field-line);
  position: relative;
  transition: background-color 0.18s ease, border-color 0.18s ease;
  flex: none;
}
.switch .track::after {
  content: "";
  position: absolute;
  top: 3px; left: 3px;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(10, 24, 42, 0.3);
  transition: transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
}
.switch input:checked + .track { background: var(--brand); border-color: var(--brand); }
.switch input:checked + .track::after { transform: translateX(20px); }
.switch input:focus-visible + .track { outline: 3px solid var(--brand); outline-offset: 2px; }
/* The state is also a word, so it survives a greyscale printout. */
.switch .state { font-weight: 640; font-size: var(--step--1); min-width: 3.2em; }

/* ------------------------------------------------------------- tables --- */

.table-wrap {
  /* Positioned as well as scrollable. A visually-hidden column heading is an
     absolutely positioned span, and without a positioned ancestor its
     containing block is the page itself, so it escapes the scroller, lands
     at the table's full width, and gives the whole document a horizontal
     scrollbar that nothing visible accounts for. */
  position: relative;
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}
table { border-collapse: collapse; width: 100%; }
caption {
  text-align: left;
  padding: var(--s4) var(--s5) var(--s3);
  color: var(--ink-2);
  font-size: var(--step--1);
}
th {
  text-align: left;
  font-size: 0.74rem;
  font-weight: 660;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: color-mix(in srgb, var(--surface-2) 60%, transparent);
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--rule-strong);
  white-space: nowrap;
}
th a { color: inherit; text-decoration: none; display: inline-flex; gap: 0.3em; align-items: center; }
th a:hover { color: var(--brand-ink); text-decoration: underline; }
td {
  padding: var(--s3) var(--s5);
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr { transition: background-color 0.1s ease; }
tbody tr:hover td { background: var(--tint); }

/* Figures line up on their digits. The column is also shrink-wrapped, because
   a number stranded at the far right of a wide column stops being next to the
   thing it describes, which is most of what makes a data table hard to read. */
.num {
  text-align: right;
  width: 0.1%;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.nowrap { white-space: nowrap; }

/* On a narrow screen a data table becomes a list of labelled rows rather than
   a horizontal scroll, because reading a spend report sideways on a phone is
   not reading it. */
@media (max-width: 700px) {
  table.stacked, table.stacked thead, table.stacked tbody,
  table.stacked th, table.stacked td, table.stacked tr { display: block; }
  table.stacked thead { position: absolute; left: -9999px; }
  table.stacked tr {
    border-bottom: 1px solid var(--rule-strong);
    padding: var(--s3) 0;
  }
  table.stacked tr:last-child { border-bottom: 0; }
  table.stacked td {
    display: grid;
    grid-template-columns: 40% 1fr;
    gap: var(--s3);
    border: 0;
    padding: var(--s2) var(--s4);
    width: auto;
  }
  table.stacked td::before {
    content: attr(data-label);
    font-size: 0.74rem;
    font-weight: 660;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-3);
  }
  table.stacked td.num { text-align: left; width: auto; }
}

/* --------------------------------------------------------- empty state --- */
/* An empty state explains what would be here and offers the way to fill it.
   It never says "No results." */

.empty {
  position: relative;
  overflow: hidden;
  text-align: center;
  padding: var(--s8) var(--s5);
  border: 1px solid var(--rule);
  border-radius: var(--radius-xl);
  background:
    radial-gradient(26rem 14rem at 32% 0%, color-mix(in srgb, var(--hue-a) 16%, transparent), transparent 70%),
    radial-gradient(24rem 13rem at 72% 6%, color-mix(in srgb, var(--hue-b) 13%, transparent), transparent 70%),
    var(--surface);
  box-shadow: var(--shadow-1);
}
.empty .icon {
  display: grid;
  place-items: center;
  width: 3.5rem; height: 3.5rem;
  margin: 0 auto var(--s4);
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--brand-ink);
  font-size: 1.5rem;
}
.empty h2, .empty h3 { margin: 0 0 var(--s2); font-size: var(--step-1); }
.empty p { margin: 0 auto var(--s5); color: var(--ink-2); max-width: 46ch; }

/* ------------------------------------------------------- money display --- */

.amount { font-variant-numeric: tabular-nums; white-space: nowrap; }
.amount-lg {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 690;
  letter-spacing: -0.028em;
}
/* Where the price is a sentence rather than a figure, "from £4.50 for each
   pupil", it still leads, but it does not get to be as loud as a number. */
.amount-md {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 660;
  letter-spacing: -0.014em;
}

/* The net / VAT / gross triple. Tenet 03 requires all three before any
   commitment, so there is a component for it and no way to show only one. */
.totals {
  border-top: 1px solid var(--rule-strong);
  padding-top: var(--s4);
  margin-top: var(--s4);
}
.totals dl { display: grid; grid-template-columns: 1fr auto; gap: var(--s2) var(--s5); margin: 0; }
.totals dt { color: var(--ink-2); }
.totals dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.totals .gross dt, .totals .gross dd {
  font-size: var(--step-1);
  font-weight: 690;
  padding-top: var(--s3);
  margin-top: var(--s1);
  border-top: 1px solid var(--rule);
}
.totals-narrow { max-width: 22rem; margin-left: auto; }

/* The calculation disclosure beside a per-pupil price. */
details.calc { margin-top: var(--s3); }
details.calc summary {
  cursor: pointer;
  font-size: var(--step--1);
  color: var(--brand-ink);
  font-weight: 600;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  padding-block: 0.2rem;
}
details.calc summary::-webkit-details-marker { display: none; }
details.calc summary::before { content: "▸"; transition: transform 0.14s ease; }
details.calc[open] summary::before { transform: rotate(90deg); }
details.calc .body {
  margin-top: var(--s3);
  padding: var(--s4);
  background: var(--surface-2);
  border-radius: var(--radius);
  font-size: var(--step--1);
}
details.calc dl { display: grid; grid-template-columns: 1fr auto; gap: var(--s2) var(--s4); margin: 0; }
details.calc dt { color: var(--ink-2); }
details.calc dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------- definition --- */

/* A definition list goes to two columns when there is room for two, and the
   thing that has to have room is the card it is in, not the window.
//
   It used to ask the window, through @media (min-width: 640px). On a wide page
   that put a label column of up to 14rem inside a 250px sidebar card, leaving
   about 90px for the value: "SG-ADVICE-2026" broke across three lines and
   "One academic year" across three more. The window was wide; the card was
   not; a media query cannot tell the two apart.

   So the card asks about itself. container-type is put only on cards that
   actually hold a definition list, which keeps the containment it implies away
   from the hundred cards that do not need it. A browser without container
   queries ignores the block and leaves every list stacked: taller, still
   perfectly readable, and never overlapping. */
.card:has(> .deflist) { container-type: inline-size; }

.deflist { display: grid; gap: var(--s3); margin: 0; }
.deflist dt { color: var(--ink-2); font-size: var(--step--1); font-weight: 600; }
.deflist dd { margin: 0 0 var(--s2); }
/* A code, a reference or an address has nowhere to break unless it is told it
   may break anywhere, and a value that cannot break is a value that escapes
   its card. */
.deflist dd { overflow-wrap: anywhere; }

@container (min-width: 22rem) {
  .deflist { grid-template-columns: minmax(7rem, 12rem) 1fr; gap: var(--s3) var(--s5); }
  .deflist dd { margin-bottom: 0; }
}

/* --------------------------------------------------------- progress --- */

.progress {
  height: 8px;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  overflow: hidden;
}
.progress .bar {
  height: 100%;
  background: linear-gradient(90deg, var(--hue-a), var(--hue-b));
  border-radius: var(--radius-pill);
  transition: width 0.25s ease;
}

.steps { display: flex; flex-wrap: wrap; gap: var(--s2); list-style: none; margin: 0 0 var(--s5); padding: 0; }
.steps li {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  border-radius: var(--radius-pill);
  font-size: var(--step--1);
  color: var(--ink-2);
  background: var(--surface-2);
}
.steps li .n {
  width: 22px; height: 22px;
  border-radius: 50%;
  display: grid; place-items: center;
  background: var(--rule-strong);
  color: var(--surface);
  font-size: 0.72rem;
  font-weight: 700;
  flex: none;
}
.steps li[aria-current] { background: var(--accent-soft); color: var(--brand-ink); font-weight: 640; }
.steps li[aria-current] .n { background: var(--brand); color: var(--on-brand); }
.steps li.done .n { background: var(--good); color: var(--on-brand); }

/* ------------------------------------------------------------- filters --- */

.filters {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  margin-bottom: var(--s5);
  box-shadow: var(--shadow-1);
}
.filters .fields {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
  align-items: stretch;
}
.filters .field { margin: 0; height: 100%; }
.filters label { font-size: var(--step--1); }

.chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 0.35rem 0.8rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--field-line);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--step--1);
  font-weight: 560;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease, color 0.12s ease;
}
.chip:hover { border-color: var(--brand); color: var(--ink); text-decoration: none; }
.chip[aria-pressed="true"] {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--on-brand);
  box-shadow: var(--shadow-brand);
}

/* ---------------------------------------------------------- pagination --- */

.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin-top: var(--s5);
  font-size: var(--step--1);
}
.pagination .pages { display: flex; gap: var(--s2); }
.pagination a, .pagination span {
  padding: var(--s2) var(--s3);
  border-radius: var(--radius);
  border: 1px solid var(--field-line);
  text-decoration: none;
  color: var(--ink-2);
  min-width: 44px;
  text-align: center;
}
.pagination a:hover { border-color: var(--brand); color: var(--brand-ink); }
.pagination [aria-current="page"] { background: var(--brand); border-color: var(--brand); color: var(--on-brand); font-weight: 650; }
.pagination .disabled { opacity: 0.45; }

/* --------------------------------------------------------------- tabs --- */

.tabs {
  position: relative;
  display: flex;
  gap: var(--s1);
  border-bottom: 1px solid var(--rule);
  overflow-x: auto;
  margin-bottom: var(--s5);
}
.tabs a {
  padding: var(--s3) var(--s4);
  text-decoration: none;
  color: var(--ink-2);
  font-weight: 580;
  white-space: nowrap;
  border-bottom: 3px solid transparent;
  margin-bottom: -1px;
}
.tabs a:hover { color: var(--ink); }
.tabs a[aria-current="page"] { color: var(--brand-ink); border-bottom-color: var(--brand); }

/* -------------------------------------------------------------- media --- */

.video {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--surface-3);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-2);
}
.video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.rich > * + * { margin-top: var(--s4); }
.rich h2 { font-size: var(--step-2); margin-top: var(--s6); }
.rich h3 { font-size: var(--step-1); margin-top: var(--s5); }
.rich ul, .rich ol { padding-left: var(--s5); }
.rich li + li { margin-top: var(--s2); }
.rich blockquote {
  margin: 0;
  padding: var(--s2) 0 var(--s2) var(--s5);
  border-left: 3px solid var(--brand);
  color: var(--ink-2);
  font-size: var(--step-1);
  line-height: 1.5;
}
.rich table { margin-block: var(--s4); }
.rich img { border-radius: var(--radius-lg); box-shadow: var(--shadow-2); }
.rich a { font-weight: 560; }

/* ------------------------------------------------------------- badges --- */

.badge {
  display: inline-grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--bad);
  color: var(--on-brand);
  font-size: 0.72rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* ------------------------------------------------------------- footer --- */

.footer {
  position: relative;
  flex: none;
  margin-top: var(--s8);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  border-top: 1px solid var(--rule);
  padding-block: var(--s7);
  font-size: var(--step--1);
  color: var(--ink-2);
}
/* A brand hairline along the top edge, fading out at both ends. It is two
   pixels of colour and it is what stops the footer reading as the page
   running out rather than finishing. */
.footer::before {
  content: "";
  position: absolute;
  inset: -1px 0 auto 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--hue-d) 16%, var(--hue-a) 40%, var(--hue-b) 64%, var(--hue-c) 86%, transparent);
  opacity: 0.8;
}
.footer .cols {
  display: grid;
  gap: var(--s5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}
.footer h2 {
  font-family: var(--font);
  font-size: 0.74rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 660;
  margin-bottom: var(--s3);
}
.footer ul { list-style: none; margin: 0; padding: 0; }
.footer li { margin-bottom: var(--s1); }

/* Standalone links get enough height to be a comfortable target on a phone.
   WCAG 2.2 allows an exception for a link inside a sentence; a list of links
   in a footer is not that, and 18px of line-height is a poor target for a
   thumb either way. The padding costs nothing on a desktop. */
.footer a {
  color: var(--ink-2);
  text-decoration: none;
  display: inline-block;
  padding-block: 0.2rem;
  min-height: 1.5rem;
}
.footer a:hover { color: var(--brand-ink); text-decoration: underline; }

/* The same reasoning wherever a link stands on its own rather than sitting in
   a sentence: beside a section heading, in a breadcrumb, in a list of links,
   or as the value in a definition list. A link inside prose keeps the line
   height it should have, which is the exception WCAG 2.2 allows for. */
.section-head p a,
.section-head > a,
.breadcrumbs a,
.deflist dd > a:only-child,
/* An address or a telephone number is the thing somebody in difficulty
   actually reaches for on a phone, so it gets a proper target whether or not
   the inline exception would have covered it. */
a[href^="mailto:"],
a[href^="tel:"] {
  display: inline-block;
  padding-block: 0.2rem;
  min-height: 1.5rem;
}
.footer .legal {
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
  color: var(--ink-3);
}

/* -------------------------------------------------------------- utils --- */

.muted { color: var(--ink-2); }
.quiet { color: var(--ink-3); }
.strong { font-weight: 650; }
.mono { font-family: var(--font-mono); font-size: 0.9em; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prewrap { white-space: pre-wrap; }
.plain-list { list-style: none; margin: 0; padding: 0; }
.plain-list-scroll { list-style: none; margin: 0; padding: 0; max-height: 26rem; overflow: auto; }
.list-indent { padding-left: 1.15rem; }
.ruled-row {
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--rule);
}
.ruled-row:last-child { border-bottom: 0; }
.ruled-row-roomy { padding-block: var(--s3); }
.ruled-row-sm { font-size: var(--step--1); }
.pad-block { padding-block: var(--s7); }
.lede-sm { font-size: var(--step-1); color: var(--ink-2); }
.unit-note { font-family: var(--font); font-size: var(--step-0); font-weight: 400; letter-spacing: 0; }
.check-lg { width: 22px; height: 22px; accent-color: var(--brand); }
.push-foot { margin-top: auto; padding-top: var(--s3); border-top: 1px solid var(--rule); }
.plain-link { text-decoration: none; }
.inline-form { display: inline-block; margin-right: var(--s3); }
.code-block {
  white-space: pre-wrap;
  font-size: 0.82rem;
  line-height: 1.5;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--s4);
  overflow-x: auto;
}
.mw-44 { max-width: 44rem; }
.mw-56 { max-width: 56rem; }
.mt0 { margin-top: 0 !important; }
.mb0 { margin-bottom: 0 !important; }
.mb1 { margin-bottom: var(--s1); }
.mb2 { margin-bottom: var(--s2); }
.mb4 { margin-bottom: var(--s4); }
.mb6 { margin-bottom: var(--s6); }
.mt3 { margin-top: var(--s3); }
.mt4 { margin-top: var(--s4); }
.mt5 { margin-top: var(--s5); }
.mt6 { margin-top: var(--s6); }
.gap-1 { gap: var(--s1); }
.gap-2 { gap: var(--s2); }
.hide { display: none !important; }
@media (max-width: 700px) { .hide-sm { display: none !important; } }
@media (min-width: 701px) { .only-sm { display: none !important; } }

/* A width the server can set without an inline style attribute, which the
   content security policy would discard. Five per cent is finer than the eye
   reads on a progress bar, and the exact figure is always stated in words
   beside it. */
.w-0 { width: 0%; } .w-5 { width: 5%; } .w-10 { width: 10%; }
.w-15 { width: 15%; } .w-20 { width: 20%; } .w-25 { width: 25%; }
.w-30 { width: 30%; } .w-35 { width: 35%; } .w-40 { width: 40%; }
.w-45 { width: 45%; } .w-50 { width: 50%; } .w-55 { width: 55%; }
.w-60 { width: 60%; } .w-65 { width: 65%; } .w-70 { width: 70%; }
.w-75 { width: 75%; } .w-80 { width: 80%; } .w-85 { width: 85%; }
.w-90 { width: 90%; } .w-95 { width: 95%; } .w-100 { width: 100%; }

/* ------------------------------------------------------------- aurora --- */

/* A slow drift of coloured light behind the artwork on the hero and the
   sign-in panel. Three blobs, three clocks, and nothing that moves is
   anything but a transform: no layout is recalculated for any of it, and the
   whole thing is clipped by the panel it sits in.

   It sits at z-index -2, which inside the panel stacking context is above the
   panel background and below the artwork, so the drawn lines stay crisp on
   top of the moving colour. */
.aurora {
  position: absolute;
  z-index: -2;
  pointer-events: none;
  inset: -35% -20%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--hue-b) 62%, transparent), transparent 72%);
  opacity: 0.7;
  animation: aurora-a 34s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
}
.aurora::before,
.aurora::after {
  content: "";
  position: absolute;
  border-radius: 50%;
}
.aurora::before {
  inset: 12% -18% -22% 22%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--hue-d) 66%, transparent), transparent 70%);
  opacity: 0.75;
  animation: aurora-b 43s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
}
.aurora::after {
  inset: -6% 6% 46% 54%;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--hue-c) 46%, transparent), transparent 68%);
  opacity: 0.5;
  animation: aurora-c 27s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
}

@keyframes aurora-a {
  from { transform: translate3d(-6%, -4%, 0) scale(1); }
  to   { transform: translate3d(9%, 7%, 0) scale(1.18); }
}
@keyframes aurora-b {
  from { transform: translate3d(7%, 6%, 0) scale(1.08); }
  to   { transform: translate3d(-9%, -7%, 0) scale(0.92); }
}
@keyframes aurora-c {
  from { transform: translate3d(4%, -5%, 0) scale(0.9); opacity: 0.34; }
  to   { transform: translate3d(-7%, 8%, 0) scale(1.16); opacity: 0.62; }
}

/* The rings.

   These are drawn inline in the page rather than in static/img/hero.svg,
   because an SVG used as a background-image renders in secure static mode,
   which switches off declarative animation as well as scripting, so a circle
   in that file can never move however it is written. Inline, each ring is an
   element the stylesheet can reach.

   They sit at z-index -2: above the drifting colour of the aurora, below the
   rest of the drawn artwork. */
.rings {
  position: absolute;
  z-index: -2;
  pointer-events: none;
  color: #fff;
  overflow: visible;
  /* Bigger than the panel and hung off its top right corner, so what shows is
     the inside of a much larger figure rather than a circle sitting on a
     rectangle. */
  width: clamp(26rem, 46vw, 48rem);
  height: clamp(26rem, 46vw, 48rem);
  top: 42%;
  right: 0;
  translate: 26% -50%;
}
.rings circle,
.rings path {
  fill: none;
  stroke: currentColor;
  /* Every ring turns and breathes about the one centre they share. */
  transform-box: view-box;
  transform-origin: 300px 300px;
}

/* Three rings, breathing on three clocks that do not divide into each other,
   so the set never visibly repeats. */
.ring-1 { stroke-width: 1.5; stroke-opacity: 0.2; animation: ring-breathe-a 19s ease-in-out infinite alternate; }
.ring-2 { stroke-width: 1.5; stroke-opacity: 0.13; animation: ring-breathe-b 27s ease-in-out infinite alternate; }
.ring-3 { stroke-width: 1.5; stroke-opacity: 0.08; animation: ring-breathe-c 33s ease-in-out infinite alternate; }

/* Two arcs on the same centres, turning slowly and in opposite directions,
   the way a pair of dials would. Ninety seconds is four degrees a second: you
   cannot watch it move, only notice it has. */
.arc-1 { stroke-width: 3; stroke-opacity: 0.5; stroke-linecap: round; animation: dial-turn 96s linear infinite; }
.arc-2 { stroke-width: 2.5; stroke-opacity: 0.28; stroke-linecap: round; animation: dial-turn-back 148s linear infinite; }

/* One mark travelling the inner ring, as slowly as the hour hand it is.
   Scoped under .rings so it outweighs the fill:none that every other circle
   in the figure needs: a bare .ring-mark loses that cascade and the mark is
   drawn with nothing in it. */
.rings .ring-mark {
  fill: currentColor;
  stroke: none;
  fill-opacity: 0.55;
  transform-box: view-box;
  transform-origin: 300px 300px;
  animation: dial-turn 72s linear infinite;
}

@keyframes ring-breathe-a {
  from { scale: 1; stroke-opacity: 0.2; }
  to   { scale: 1.035; stroke-opacity: 0.3; }
}
@keyframes ring-breathe-b {
  from { scale: 1.025; stroke-opacity: 0.13; }
  to   { scale: 1; stroke-opacity: 0.2; }
}
@keyframes ring-breathe-c {
  from { scale: 1; stroke-opacity: 0.08; }
  to   { scale: 1.045; stroke-opacity: 0.14; }
}
@keyframes dial-turn {
  from { rotate: 0deg; }
  to   { rotate: 360deg; }
}
@keyframes dial-turn-back {
  from { rotate: 0deg; }
  to   { rotate: -360deg; }
}

/* On the sign-in panel the same figure sits higher and smaller, because the
   panel is a tall column rather than a wide band. */
.auth-aside .rings {
  width: clamp(20rem, 60vw, 30rem);
  height: clamp(20rem, 60vw, 30rem);
  top: 0;
  right: 0;
  translate: 30% -34%;
}

@keyframes wash-drift {
  from { transform: translate3d(-1.5%, -1%, 0) scale(1); }
  to   { transform: translate3d(1.5%, 1.5%, 0) scale(1.07); }
}
@keyframes grid-breathe {
  from { opacity: 0.55; }
  to   { opacity: 1; }
}
/* ---------------------------------------------------------- on scrolling -- */

/* Two effects driven by the scroll position rather than by a clock. Neither
   needs a script, a listener or a layout read, and where the browser has no
   scroll timeline both simply do not exist.

   Only the transform is animated. An opacity reveal would mean content that
   is invisible until it has been scrolled past, and a timeline that never
   advances (a print pipeline, a snapshot service) would take the page with
   nothing on it. Sixteen pixels of travel cannot fail that way. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes section-in {
      from { transform: translateY(16px); }
    }
    /* Sections only. A sidebar-layout column holds the sticky side nav, and
       a sticky box inside a box that is being translated moves with it:
       the nav would drift while its own section scrolled into place. */
    .section {
      animation: section-in linear both;
      animation-timeline: view();
      animation-range: entry 8% cover 24%;
    }
  }
}

/* How far through the page you are, as a hairline under the masthead. It is
   decoration: the same answer is in the scrollbar. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes read-progress {
      from { transform: scaleX(0); }
      to   { transform: scaleX(1); }
    }
    .masthead::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: -1px;
      height: 2px;
      transform-origin: left center;
      background: linear-gradient(90deg, var(--hue-d), var(--hue-a) 40%, var(--hue-b) 72%, var(--hue-c));
      animation: read-progress linear both;
      animation-timeline: scroll();
    }
  }
}

/* ------------------------------------------------------------ entrances -- */

/* Everything that arrives with the page arrives from a little below it, over
   less than half a second. Two rules make this safe rather than clever:
   the starting state lives in the keyframe and never on the element, so a
   browser that runs no animation shows the page rather than a blank one; and
   the delays are removed outright under reduced motion, because a delay with
   a zero-length animation is just a pause on nothing. */
@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
}
@keyframes rise-sm {
  from { opacity: 0; transform: translateY(6px); }
}
@keyframes fade {
  from { opacity: 0; }
}
@keyframes grow-x {
  from { transform: scaleX(0); }
}

.hero,
.page-head,
.auth-form,
.auth-aside {
  animation: rise 0.5s cubic-bezier(0.16, 0.84, 0.32, 1) backwards;
}
.auth-aside { animation-delay: 0.08s; }

.flashes .notice { animation: rise-sm 0.35s cubic-bezier(0.16, 0.84, 0.32, 1) backwards; }

/* A grid of cards deals itself out rather than appearing all at once. Eight
   steps is enough: past that the last card would be waiting long enough to
   notice, so the ninth onwards simply arrive with the eighth. */
.grid > .linkcard,
.stats > .stat,
.hero-facts > div {
  animation: rise-sm 0.42s cubic-bezier(0.16, 0.84, 0.32, 1) backwards;
}
.grid > .linkcard:nth-child(2), .stats > .stat:nth-child(2), .hero-facts > div:nth-child(2) { animation-delay: 0.05s; }
.grid > .linkcard:nth-child(3), .stats > .stat:nth-child(3), .hero-facts > div:nth-child(3) { animation-delay: 0.1s; }
.grid > .linkcard:nth-child(4), .stats > .stat:nth-child(4) { animation-delay: 0.15s; }
.grid > .linkcard:nth-child(5), .stats > .stat:nth-child(5) { animation-delay: 0.2s; }
.grid > .linkcard:nth-child(6), .stats > .stat:nth-child(6) { animation-delay: 0.25s; }
.grid > .linkcard:nth-child(7), .stats > .stat:nth-child(7) { animation-delay: 0.3s; }
.grid > .linkcard:nth-child(n + 8), .stats > .stat:nth-child(n + 8) { animation-delay: 0.35s; }

/* A bar that fills is worth watching fill. Scaling rather than animating the
   width means the class the server set still decides where it stops. */
.progress .bar,
.meter > span {
  transform-origin: left center;
  animation: grow-x 0.85s cubic-bezier(0.22, 0.8, 0.28, 1) both;
}

/* The underline under the section you are in draws itself. */
.mainnav a[aria-current="page"]::after {
  transform-origin: left center;
  animation: grow-x 0.45s cubic-bezier(0.22, 0.8, 0.28, 1) both;
}

/* The worked calculation beside a per-pupil price. */
details.calc[open] .body { animation: fade 0.22s ease backwards; }

/* A masthead that lifts off the page as you scroll past it. Scroll-driven
   animation needs no script and no scroll listener, so where the browser has
   it the effect costs nothing, and where it does not the bar simply stays
   flat, which is what it did before. */
@supports (animation-timeline: scroll()) {
  @keyframes masthead-lift {
    to { box-shadow: var(--shadow-2); }
  }
  @media (min-width: 900px) {
    .masthead {
      animation: masthead-lift auto linear both;
      animation-timeline: scroll();
      animation-range: 0 6rem;
    }
  }
}

/* ------------------------------------------------------------ motion --- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  a.linkcard:hover, a.linkcard:focus-visible { transform: none; }
  /* Every drifting layer stops where it started rather than snapping to the
     end of its travel, and each panel keeps its colour. The band of light
     rests at zero opacity, which is where it is designed to rest. */
  .aurora, .aurora::before, .aurora::after,
  .rings circle, .rings path,
  .card-feature::after,
  body::before, body::after { animation: none !important; }
}

/* ------------------------------------------------------------- print --- */
/* A school prints its spend report and takes it to a governors' meeting. */

@media print {
  :root { --ground: #fff; --surface: #fff; --ink: #000; --ink-2: #333; --rule: #ccc; --tint: transparent; }
  /* Nothing arrives, because there is nowhere to arrive from. An entrance
     animation begins at zero opacity, and a print or snapshot context whose
     clock never advances would otherwise commit that first frame to paper. */
  *, *::before, *::after { animation: none !important; }
  .aurora, .rings { display: none !important; }
  .masthead::after, .card-feature::after { display: none !important; }
  body::before, body::after { display: none !important; }
  .masthead, .mainnav, .footer, .skip, .filters, .pagination, .btn, .acting-banner { display: none !important; }
  body { background: #fff; font-size: 11pt; }
  .card, .table-wrap, .stat, .linkcard { border-color: #999; box-shadow: none; break-inside: avoid; }
  .hero { background: #fff !important; color: #000; box-shadow: none; border: 1px solid #999; }
  .hero::before, .hero::after { display: none; }
  .hero .lede, .hero .eyebrow, .hero-facts .label { color: #333; }
  a { color: #000; text-decoration: underline; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #555; }
  .pill { border: 1px solid #666; }
  table { font-size: 10pt; }
  thead { display: table-header-group; }
}

/* --------------------------------------------------------- the aside --- */

/* Sign-in, invitation, password reset. One column of form beside one panel of
   artwork: the page a school sees before it sees anything else, so it is the
   page that has to look like somebody meant it. */
.auth {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s6);
  align-items: stretch;
  padding-block: var(--s6) var(--s8);
}
@media (min-width: 920px) {
  .auth { grid-template-columns: minmax(0, 1fr) minmax(0, 26rem); gap: var(--s8); }
}
.auth-form { max-width: 34rem; width: 100%; }
.auth-form h1 { margin-bottom: var(--s2); }
.auth-form .lede { color: var(--ink-2); font-size: var(--step-1); margin-bottom: var(--s6); }

/* The panel beside the sign-in form: what a school can do for itself.
   
   It was a deep gradient with white type, and the decoration was drawn in white
   over it -- which meant the artwork sat behind the words rather than around
   them, and the list was the hardest thing on the site to read. It is a light
   panel with ordinary ink now, which is both lighter and a good deal more
   legible than white on a moving gradient could be.

   The words also sit at the top rather than the bottom. Reading starts at the
   top of a column, and pinning the text to the bottom of a 26rem panel put a
   third of the page's argument below where anybody looks first. What fills the
   space it left is the decoration, which is a better use of it. */
.auth-aside {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--radius-xl);
  padding: var(--s6);
  min-height: 26rem;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  /* Near-white where the words are and tinted where the artwork is. The page
     behind this panel is itself a pale wash, so a panel tinted all through
     disappears into it; the reading half stays plain and the colour goes where
     there is nothing to read. */
  background-color: var(--surface);
  background-image: linear-gradient(180deg,
    var(--surface) 44%,
    color-mix(in srgb, var(--hue-a) 9%, var(--surface)) 78%,
    color-mix(in srgb, var(--hue-b) 12%, var(--surface)));
  color: var(--ink);
  border: 1px solid var(--rule-strong);
  box-shadow: var(--shadow-3);
}

/* The artwork, kept and re-tinted rather than dropped. It is a white drawing,
   so it is used as a mask and painted with the brand instead of being drawn
   directly: one file still serves both themes and any hue a council picks,
   which is the property it was made for.

   It occupies the lower part of the panel only, so it is beside the words and
   never behind them. That is the half of "easier to read" a colour change on
   its own does not fix. */
.auth-aside::before {
  content: "";
  position: absolute;
  inset: 42% 0 0;
  z-index: -1;
  background: var(--brand);
  opacity: 0.16;
  -webkit-mask: url("/static/img/aside.svg") center bottom / cover no-repeat;
  mask: url("/static/img/aside.svg") center bottom / cover no-repeat;
}
.auth-aside h2 {
  margin: 0 0 var(--s3);
  font-size: var(--step-2);
  letter-spacing: -0.022em;
}
.auth-aside p { color: var(--ink-2); margin-bottom: var(--s4); }
.auth-aside ul {
  list-style: none;
  margin: 0;
  padding: 0;
  color: var(--ink-2);
}
.auth-aside li {
  position: relative;
  padding-left: var(--s5);
  margin-bottom: var(--s3);
  font-size: var(--step--1);
}
.auth-aside li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--brand);
  font-weight: 700;
}

/* Both decorative layers were drawn for a dark ground and are hung where the
   words now are. Tinted, faded and pushed below them. */
.auth-aside .aurora { opacity: 0.22; inset: 46% -20% -35%; }
.auth-aside .rings {
  color: var(--brand);
  opacity: 0.5;
  top: auto;
  bottom: 0;
  right: 0;
  translate: 26% 42%;
}

@media (max-width: 919px) {
  .auth-aside { min-height: 0; }
  /* Below the breakpoint the panel sits under the form and is only as tall as
     its words, so there is no space beneath them for anything to occupy. The
     drawn layers would cross the list rather than sit under it, so the wash is
     all that is left and the two figures go entirely. */
  .auth-aside::before { inset: 0; opacity: 0.07; }
  .auth-aside .aurora,
  .auth-aside .rings { display: none; }
}

/* A single-column page of prose or one form, centred in the measure. */
.narrow-page { max-width: 34rem; padding-block: var(--s7); }
.narrow-page-wide { max-width: 42rem; padding-block: var(--s7); }

/* ---------------------------------------------------------- e-learning -- */
/* The module player. Two columns on a desktop, one on a phone, and the same
   markup either way: the contents list simply moves above the step rather
   than being hidden, because somebody on a phone still needs to know how much
   is left. */
.player {
  display: grid;
  gap: var(--s6);
  grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
  align-items: start;
  padding-block: var(--s5);
}
.player-side {
  position: sticky;
  top: calc(var(--header-h) + var(--s4));
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--s5);
  box-shadow: var(--shadow-1);
}
.player-main { min-width: 0; }
.player-main > h1 { margin-top: var(--s1); }

.player-steps { list-style: none; margin: var(--s4) 0 0; padding: 0; }
.player-steps li + li { margin-top: var(--s1); }
.player-steps a {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  padding: var(--s2);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--ink);
}
.player-steps a:hover { background: var(--surface-2); }
.player-steps a.here { background: var(--accent-soft); font-weight: 620; }
.player-steps .marker {
  flex: 0 0 1.6rem;
  height: 1.6rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--ink-2);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}
.player-steps a.here .marker { background: var(--brand); color: var(--on-brand); }
.player-steps .label { display: block; font-size: var(--step--1); line-height: 1.35; }
.player-steps .label .quiet { display: block; font-size: 0.76rem; }

/* A progress meter, labelled for screen readers by its container. Progress is
   also stated in words next to it, because a bar alone carries meaning by
   length and colour only. */
.meter {
  height: 0.5rem;
  border-radius: var(--radius-pill);
  background: var(--surface-3);
  overflow: hidden;
  margin-block: var(--s3) var(--s2);
}
.meter > span {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--hue-a), var(--hue-b));
  border-radius: var(--radius-pill);
}
.meter-sm { height: 0.35rem; margin-block: 0 var(--s1); max-width: 8rem; }

.embed {
  position: relative;
  aspect-ratio: 16 / 9;
  margin-block: var(--s4);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: #000;
  box-shadow: var(--shadow-2);
}
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.filecard {
  display: flex;
  gap: var(--s4);
  align-items: center;
  padding: var(--s4) var(--s5);
  margin-block: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
  box-shadow: var(--shadow-1);
  transition: border-color 0.14s ease, box-shadow 0.14s ease, transform 0.14s ease;
}
.filecard:hover {
  border-color: color-mix(in srgb, var(--brand) 30%, var(--rule));
  box-shadow: var(--shadow-2);
  transform: translateY(-1px);
}
.filecard-icon {
  flex: 0 0 2.5rem; height: 2.5rem;
  display: grid; place-items: center;
  border-radius: 999px; background: var(--accent-soft); color: var(--brand-ink);
}
.filecard .quiet { display: block; font-size: var(--step--1); }

.stepnav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
}

/* The knowledge check, and the marked version of it. */
.quiz { margin-top: var(--s4); }
/* A knowledge-check question is a sentence, not a label, so it is a paragraph
   and not the fieldset's <legend>. The fieldset is named by it with
   aria-labelledby instead, which is the same accessible name by another route.

   A <legend> is drawn ON its fieldset's border with the border cut away behind
   it. That suits a one-word label and breaks the moment the text wraps: the
   first line ends up above the box, so the question is outside the card it
   belongs to. Every CSS way round it was tried in a real browser and measured
   -- float, position, display on the legend, and block, grid and flex on the
   fieldset -- and Chrome kept the legend on the border through all six. The
   control in that measurement was swapping the legend for a paragraph, which is
   what this now does.

   Nothing in the test suite saw it. The legend is inside the fieldset's own
   rectangle whichever way it is drawn, because the fieldset grows to contain
   it, so the sweep that looks for things leaving their card reports the page
   clean. It was found by looking at a screenshot. */
.question {
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--s5);
  margin-block: var(--s4);
  box-shadow: var(--shadow-1);
}
.question > .question-prompt {
  margin: 0 0 var(--s4);
  font-weight: 640;
  font-size: var(--step-1);
  line-height: 1.4;
  display: flex;
  gap: var(--s3);
  align-items: baseline;
}
.qnum {
  display: inline-grid;
  place-items: center;
  min-width: 1.7rem;
  height: 1.7rem;
  padding-inline: 0.35rem;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--ink-2);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.marked { border-left: 4px solid var(--rule); }
.marked-right { border-left-color: var(--good); }
.marked-wrong { border-left-color: var(--warn); }

.answers { list-style: none; margin: var(--s4) 0 0; padding: 0; }
.answers li {
  display: flex;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius);
  border: 1px solid var(--rule);
}
.answers li + li { margin-top: var(--s2); }
.answers li.is-right { border-color: var(--good); background: var(--good-soft); }
.answers li.is-chosen:not(.is-right) { border-color: var(--bad); background: var(--bad-soft); }
.answers .mark {
  flex: 0 0 1.4rem; height: 1.4rem;
  display: grid; place-items: center;
  font-weight: 700;
}
.answers li.is-right .mark { color: var(--good); }
.answers li.is-chosen:not(.is-right) .mark { color: var(--bad); }
.answer-label { margin: 0; font-weight: 600; }
.answer-verdict { margin: var(--s1) 0 0; font-size: var(--step--1); color: var(--ink-2); }
.answer-feedback { margin: var(--s2) 0 0; font-size: var(--step--1); }

.minifacts { display: grid; gap: var(--s2); margin: var(--s3) 0; }
.minifacts div { display: flex; gap: var(--s2); flex-wrap: wrap; font-size: var(--step--1); }
.minifacts dt { color: var(--ink-3); margin: 0; }
.minifacts dt::after { content: ":"; }
.minifacts dd { margin: 0; font-weight: 560; }

.notice-tight { padding: var(--s3) var(--s4); margin-block: var(--s3); }
.notice-tight .notice-title { font-size: var(--step-0); }
.card-item { display: flex; flex-direction: column; }
.card-item .actions { margin-top: auto; padding-top: var(--s3); }
.row-tight { gap: var(--s2); margin-bottom: var(--s2); }

@media (max-width: 60rem) {
  .player { grid-template-columns: minmax(0, 1fr); }
  .player-side { position: static; }
}

/* ------------------------------------------------ conversations and stages -- */
/* A message thread on a quote. Two sides, distinguished by position and by a
   label rather than by colour alone. */
.thread { list-style: none; margin: 0 0 var(--s5); padding: 0; }
.thread li {
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--s4) var(--s5);
  background: var(--surface);
  max-width: 44ch;
  box-shadow: var(--shadow-1);
}
.thread li + li { margin-top: var(--s3); }
.thread li.from-council { background: var(--accent-soft); border-color: transparent; }
.thread li.from-school { margin-left: auto; }
.thread-meta { margin: 0 0 var(--s1); font-size: var(--step--1); color: var(--ink-3); }
.thread-body { margin: 0; white-space: pre-wrap; }
.side-council { border-left: 4px solid var(--brand); }
.side-school { border-left: 4px solid var(--info); }

/* Where something has reached, as a list rather than a graphic, so it reads
   the same to a screen reader as it does on the page. */
.progress-list { list-style: none; margin: 0 0 var(--s4); padding: 0; }
.progress-list li {
  position: relative;
  padding-left: var(--s5);
  padding-block: var(--s2);
  color: var(--ink-3);
}
.progress-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.95rem;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 999px;
  border: 2px solid var(--rule-strong);
  background: var(--surface);
}
.progress-list li + li::after {
  content: "";
  position: absolute;
  left: 0.32rem;
  top: -0.4rem;
  width: 2px;
  height: 1.4rem;
  background: var(--rule);
}
.progress-list li.done { color: var(--ink); font-weight: 560; }
.progress-list li.done::before { background: var(--brand); border-color: var(--brand); }

/* --------------------------------------------------- forums and reports -- */
.card-unread { border-left: 4px solid var(--info); }

.posts { list-style: none; margin: 0 0 var(--s6); padding: 0; counter-reset: post; }
.posts li {
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--s5);
  box-shadow: var(--shadow-1);
}
.posts li + li { margin-top: var(--s3); }
.posts li.is-hidden { background: var(--surface-2); border-style: dashed; }
.post-meta { margin: 0 0 var(--s2); font-size: var(--step--1); color: var(--ink-3); }
.post-body { white-space: pre-wrap; }

/* Reports are wide and numeric, so the numbers line up and the total row is
   visibly a total rather than another row. */
.report { font-variant-numeric: tabular-nums; }
.report tfoot th {
  border-top: 2px solid var(--rule-strong);
  font-weight: 680;
  background: var(--surface-2);
  font-size: var(--step--1);
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink);
}

/* --------------------------------------------------------- the builder -- */
/* The list of steps or questions in the module builder: what it is, and what
   can be done to it, side by side. */
.builder { list-style: none; margin: 0; padding: 0; counter-reset: step; }
.builder li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
  justify-content: space-between;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
}
.builder li + li { margin-top: var(--s2); }
.builder-head { display: flex; gap: var(--s3); align-items: center; min-width: 0; }
.builder-head .quiet { font-size: var(--step--1); }
.builder-actions { display: flex; gap: var(--s2); align-items: center; flex-wrap: wrap; }
.builder-actions form { margin: 0; }

.answers-editor {
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  margin-block: var(--s4);
}
.answers-editor legend { font-weight: 640; padding-inline: var(--s2); }
.answer-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) minmax(0, 1.4fr);
  gap: var(--s4);
  align-items: stretch;
  padding-block: var(--s3);
}
.answer-row + .answer-row { border-top: 1px solid var(--rule); }
.choice-inline { padding: 0; border: 0; background: none; margin-top: 1.85rem; }

@media (max-width: 48rem) {
  .answer-row { grid-template-columns: auto minmax(0, 1fr); }
  .answer-row .field:last-child { grid-column: 2; }
  .choice-inline { margin-top: 0.6rem; }
}

.label-like {
  display: block;
  font-weight: 620;
  margin-bottom: var(--s2);
  font-size: var(--step--1);
}

/* The rich-text surface the script puts in front of the textarea. Without
   these it is a run of unboxed text sitting where a field should be: the
   textarea it replaces is hidden, so nothing else draws the edge. It is
   deliberately the same box as every other control on the page, because it is
   one. */
.editor-surface {
  min-height: 12rem;
  padding: 0.65rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--field-line);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 2px rgba(10, 24, 42, 0.04);
  overflow-y: auto;
  transition: border-color 0.14s ease, box-shadow 0.14s ease;
}
.editor-surface:focus {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--accent-ring);
  outline: none;
}
.editor-surface > :first-child { margin-top: 0; }

/* A basket line the school has unticked for a partial checkout. It is still
   there and still says what it costs; it is simply not in this order, and the
   tick is not the only thing that says so. */
tr.line-off td { opacity: 0.55; }
tr.line-off td:first-child { box-shadow: inset 3px 0 0 var(--rule-strong); }

/* A SCORM package runs in its own frame at a sensible size for a course. */
.scorm-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  min-height: 30rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: #fff;
  box-shadow: var(--shadow-2);
}
.scorm-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.preview-frame {
  width: 100%;
  height: 44rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: #fff;
}

@media (max-width: 48rem) {
  .scorm-frame { aspect-ratio: auto; height: 70vh; min-height: 24rem; }
}

/* ==========================================================================
   Arranged pages
   ==========================================================================
   The section editor and the section types it draws. Everything here is a
   class because the stylesheet has no inline escape hatch: style-src names a
   nonce, and a nonce can whitelist a <style> element but never a style
   attribute, so an inline style is discarded by the browser without a word in
   any log. */

/* A parked section keeps its place in the list and says so, rather than
   vanishing and leaving somebody wondering where it went. */
.builder li.is-parked { opacity: 0.72; }
.builder li.is-parked .builder-head strong { text-decoration: line-through; }

/* -------------------------------------------------------- picture picker --- */

.image-field { display: block; }
.image-current { align-items: center; gap: var(--s4); margin-bottom: var(--s4); }
.image-current .thumb {
  width: 10rem;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--surface-2);
  flex: none;
}

.imagegrid {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr));
  margin-block: var(--s4);
  max-height: 24rem;
  overflow-y: auto;
  padding: var(--s2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.imagechoice { display: flex; flex-direction: column; gap: var(--s1); cursor: pointer; }
/* The radio itself is off-screen rather than display:none, so it keeps its
   place in the tab order and a screen reader still announces the group. */
.imagechoice input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}
.imagechoice-body {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius);
  /* Two pixels of transparent border, so selecting one does not move the
     others by the width of a border appearing. */
  border: 2px solid transparent;
  outline: 1px solid var(--field-line);
  outline-offset: -1px;
  background: var(--surface);
  display: flex;
  align-items: center;
  justify-content: center;
}
.imagechoice-none { color: var(--ink-2); font-size: var(--step--1); }
.imagechoice-label {
  font-size: var(--step--1);
  color: var(--ink-2);
  text-align: center;
  line-height: 1.3;
}
.imagechoice input:checked + .imagechoice-body,
.imagechoice input:checked ~ .imagechoice-body {
  border-color: var(--brand);
  outline-color: var(--brand);
}
.imagechoice input:focus-visible + .imagechoice-body,
.imagechoice input:focus-visible ~ .imagechoice-body {
  outline: 3px solid var(--brand);
  outline-offset: 2px;
}
.imagechoice input:checked ~ .imagechoice-label { color: var(--ink); font-weight: 600; }

/* --------------------------------------------------------- section types --- */

.section-lede { margin-bottom: var(--s5); }

.fieldset {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--s4);
  margin-block: var(--s5);
}
.fieldset > legend { padding-inline: var(--s2); font-weight: 600; }

.figure { margin: 0; }
.figure-media {
  width: 100%;
  border-radius: var(--radius-lg);
  border: 1px solid var(--rule);
  display: block;
}
.figure figcaption { margin-top: var(--s3); color: var(--ink-2); font-size: var(--step--1); }

.quote-card { display: flex; flex-direction: column; gap: var(--s3); }
.quote-body {
  margin: 0;
  font-size: var(--step-1);
  line-height: 1.5;
  /* A quotation mark drawn by the stylesheet rather than typed into the
     content, so the words in the database are the words somebody said. */
  quotes: "\201C" "\201D";
}
.quote-body::before { content: open-quote; color: var(--hue-c); }
.quote-body::after { content: close-quote; color: var(--hue-c); }
.quote-who { color: var(--ink-2); font-size: var(--step--1); margin-top: auto; }

.contact-card { display: flex; flex-direction: column; gap: var(--s3); }
/* The contact card wraps each pair in its own div, so it is a one-column list
   whatever its width; the container query above governs the rest. */
.contact-card .deflist { grid-template-columns: 1fr; gap: var(--s2); }
.contact-card .deflist > div + div { margin-top: var(--s2); }
.avatar {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  border-radius: 50%;
  background: color-mix(in srgb, var(--hue-a) 18%, var(--surface));
  color: var(--ink);
  font-weight: 700;
  font-size: var(--step--1);
  letter-spacing: 0.02em;
}

.filelist { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s3); }
.filelist > li {
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
}
.filelist > li > a { font-weight: 600; }

/* ------------------------------------------------------- the wide banner --

   A banner picture goes BESIDE the words, not behind them.

   The first version of this stretched the picture across the whole panel as a
   background under a scrim, and it looked exactly as bad as that description
   deserves: a 16:9 drawing cropped to a wide short strip is mostly sky, and
   the parts worth seeing are the parts cut off. It also took the name
   .hero-media, which three page templates were already using for the framed
   picture at the top of an article, and being later in the stylesheet, it
   won. Every one of those pages silently turned its picture into a background.

   So the picture is a card in its own column, and what sits behind the words
   is the same picture blurred past recognition and faded, which gives the
   panel its colour without asking anybody to read over detail. */

.hero-full {
  /* Edge to edge, from inside a container that is not. The negative margin is
     half the difference between the container and the viewport, so the panel
     is centred on the viewport rather than on the container. .shell clips the
     overflow this creates, which is why the page itself still never scrolls
     sideways. */
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  margin-block: 0 var(--s7);
  border-radius: 0;
  padding-inline: 0;
}
.hero-full::after { border-radius: 0; }

.hero-inner {
  position: relative;
  display: grid;
  gap: clamp(var(--s5), 4vw, var(--s7));
  align-items: center;
  padding-block: clamp(var(--s6), 5vw, var(--s8));
}
@media (min-width: 62rem) {
  .hero-split .hero-inner { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); }
}
.hero-body { min-width: 0; }
.hero-body > .hero-search { margin-top: var(--s6); }

.hero-figure { margin: 0; min-width: 0; }
.hero-portrait {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  box-shadow: var(--shadow-3);
  /* The same fine top highlight the panel itself has, so the picture reads as
     sitting on the panel rather than punched through it. */
  outline: 1px solid rgba(255, 255, 255, 0.16);
  outline-offset: -1px;
}
@media (max-width: 62rem) {
  .hero-portrait { aspect-ratio: 16 / 9; }
}

/* The picture behind the words: the same file, blurred past recognition and
   faded, so it colours the panel without competing with anything on it.
   scale(1.15) hides the soft edge a large blur leaves at the boundary. */
.hero-backdrop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -1;
  opacity: 0.3;
  filter: blur(46px) saturate(1.25);
  transform: scale(1.15);
}

/* ------------------------------------------------ a picture and its glow --

   The framed picture at the top of an article, with the same picture behind it
   blurred and faded: the colour spills out past the frame the way light does
   off a screen in a dark room. The glow is decorative and hidden from the
   accessibility tree; the frame keeps the alt text. */

.media-scene {
  position: relative;
  isolation: isolate;
  margin-bottom: var(--s5);
}
.media-scene > .hero-media { margin-bottom: 0; }
.media-scene-glow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -1;
  opacity: 0.5;
  filter: blur(40px) saturate(1.4);
  transform: scale(0.94) translateY(1.5rem);
  border-radius: var(--radius-xl);
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .media-scene-glow { transition: opacity 0.4s ease; }
}
.h-hero { font-size: var(--step-4); line-height: 1.1; margin-block: 0 var(--s4); }
.prose-invert { color: color-mix(in srgb, #fff 88%, transparent); }
.prose-invert a { color: #fff; }
.prose-sm { font-size: var(--step--1); }

.contact-mini + .contact-mini { margin-top: var(--s4); padding-top: var(--s4); border-top: 1px solid var(--rule); }


/* ------------------------------------------------------------- planner --- */
/* A month heading with its dates under it, not a grid of thirty-one boxes with
   three of them filled. A grid is mostly white space, wraps badly at 375px and
   reads terribly aloud; this reads the same way on every width. */

.planner { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s3); }
.planner-day {
  display: grid;
  grid-template-columns: 3.5rem 1fr;
  gap: var(--s4);
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
}
.planner-date {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 0.1rem;
  padding: var(--s2) 0;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--hue-a) 12%, var(--surface));
}
.planner-dow { font-size: var(--step--1); color: var(--ink-2); text-transform: uppercase; letter-spacing: 0.06em; }
.planner-dom { font-size: var(--step-2); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.planner-items { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s3); }
.planner-items > li + li { padding-top: var(--s3); border-top: 1px solid var(--rule); }

/* The banner's body text. It takes the lede's size rather than the prose
   default, because a banner is read at a glance and the paragraph in it is
   doing the same job the lede does on every other page. */
.hero .lede-prose { font-size: var(--step-1); line-height: 1.5; max-width: 46ch; }
.hero .lede-prose p { margin-block: var(--s4) 0; }
.hero .lede-prose > :first-child { margin-top: var(--s4); }

/* An address with no spaces has nowhere to break, and a sidebar is narrow. */
.contact-mini-email { overflow-wrap: anywhere; }

/* --------------------------------------------------------- site search --- */
/* A small field in the masthead, next to the account buttons. It is one
   control drawn as one shape: the icon, the input and the button share a pill,
   and the pill takes the focus ring rather than the input inside it, so what
   lights up is what a reader sees as the thing they are in. */

.masthead-search {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
  padding-inline: 0.7rem;
  min-height: 2.25rem;
  border: 1px solid var(--field-line);
  border-radius: var(--radius-pill);
  background: var(--surface);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.masthead-search:hover { border-color: var(--brand); }
/* Both this and the actions block pushed themselves right, so the free space
   split between them and the field ended up marooned in the middle. The auto
   margin belongs to whichever comes first; everything after it follows. */
.masthead-search + .masthead-actions { margin-left: 0; }
/* :focus-within rather than :focus, so the ring belongs to the pill and not to
   the bare input sitting inside it. */
.masthead-search:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 22%, transparent);
}

.masthead-search-icon {
  width: 1rem;
  height: 1rem;
  flex: none;
  fill: none;
  stroke: var(--ink-3);
  stroke-width: 2;
  stroke-linecap: round;
}
.masthead-search:focus-within .masthead-search-icon { stroke: var(--brand); }

/* The input keeps none of the field styling: the pill around it is the field.
   Height is set against .btn-sm so it lines up with the buttons beside it
   rather than against --control-h, which is half as tall again. */
.masthead-search input[type="search"] {
  width: 9rem;
  height: 2rem;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  font-size: var(--step--1);
  color: var(--ink);
}
.masthead-search input[type="search"]:focus-visible { outline: none; }
.masthead-search input[type="search"]::placeholder { color: var(--ink-3); }
/* WebKit draws its own clear button, which sits at a different height from
   everything else in the pill. */
.masthead-search input[type="search"]::-webkit-search-cancel-button { display: none; }

@media (min-width: 1100px) {
  .masthead-search input[type="search"] { width: 12rem; }
}

/* The submit button is there for anybody who does not press Return, and for a
   screen reader that wants to know how the form is sent. It is off-screen
   rather than display:none so it keeps its place in the tab order. */
.masthead-search-go {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Below the sticky breakpoint the masthead wraps, and the search takes the
   whole of its own line rather than being squeezed against the buttons. */
@media (max-width: 620px) {
  .masthead-search { order: 3; width: 100%; margin-left: 0; }
  .masthead-search input[type="search"] { width: auto; flex: 1 1 auto; }
}

/* ---------------------------------------------- setting up a second factor --

   The QR and the key sit side by side where there is room and stack where
   there is not. auto-fit with a minimum rather than a media query, because
   this block lives inside a card and a media query cannot tell a narrow card
   from a narrow window. */
.setup-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s5);
  align-items: start;
  margin-top: var(--s4);
}
.qr {
  max-width: 15rem;
  background: #ffffff;
  padding: var(--s3);
  border-radius: var(--radius);
  border: 1px solid var(--rule);
}
.qr svg { display: block; width: 100%; height: auto; }
.setup-key {
  font-size: var(--step-1);
  letter-spacing: 0.04em;
  word-break: break-all;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--s3);
}

/* Recovery codes. Shown once, so they are laid out to be read off a screen and
   typed, or printed and filed. */
.codelist {
  list-style: none;
  margin: 0 0 var(--s4);
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--s2) var(--s4);
}
.codelist li {
  font-size: var(--step-0);
  letter-spacing: 0.06em;
  padding: var(--s2) var(--s3);
  background: var(--surface-2);
  border-radius: var(--radius);
}

/* A checksum is read by comparing, not by reading, so it gets the full width
   of whatever holds it rather than half of a two-column definition list. */
.checksum {
  overflow-wrap: anywhere;
  font-size: var(--step--1);
  color: var(--ink-2);
  margin-bottom: var(--s3);
}

/* ---------------------------------------------------------------- maps --
   A place on a map: choosing one on a form, and showing one on a page.

   The map is decoration in the strict sense used everywhere else here. Every
   location is a venue name, an address and two number fields, all of which
   work with scripting off; this makes the two number fields something you
   point at. So .map has a height and a border and nothing else until map.js
   fills it, and a page that runs no script shows the address block below it
   with no gap where a map should have been.

   Tile positions are written through element.style from map.js rather than
   rendered into the markup, because style-src names a nonce and a nonce can
   whitelist a <style> element but never a style attribute: an inline one is
   discarded by the browser without a word in any log. */

.map-field { display: grid; gap: var(--s3); }

.map {
  position: relative;
  border: 1px solid var(--field-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background:
    linear-gradient(var(--quiet-soft), var(--quiet-soft)),
    var(--surface);
}

/* Nothing is drawn until the script has built the canvas, so the empty box
   never sits on the page as a bordered grey rectangle promising a map. */
.map:not(.map-live) { display: none; }

.map-canvas {
  position: relative;
  height: 22rem;
  cursor: grab;
  touch-action: none;
  overflow: hidden;
  outline-offset: -3px;
}
.map-canvas.is-dragging { cursor: grabbing; }
.map-sm .map-canvas { height: 15rem; }

.map-layer { position: absolute; inset: 0; }

.map-tile {
  position: absolute;
  top: 0;
  left: 0;
  width: 256px;
  height: 256px;
  max-width: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* The pin. Drawn from the brand so a re-brand carries it, and given a white
   ring so it reads against both a playing field and a car park.

   No rotation anywhere on this element. The conventional teardrop is a square
   with three round corners turned 45 degrees, and that cannot be combined with
   positioning it: the individual `rotate` property is applied before the
   `transform` property, so the translate that places the pin is itself
   rotated. The first version of this drew every pin 173px outside the map, at
   an angle, with nothing in any log to say so. A circle and a tail need no
   rotation and cannot go wrong that way. */
.map-pin {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  width: 20px;
  height: 20px;
  /* The tip lands on the coordinate: 20px circle plus an 8px tail. */
  margin: -28px 0 0 -10px;
  border-radius: 50%;
  background: var(--brand);
  border: 2px solid #fff;
  box-shadow: var(--shadow-2);
  pointer-events: none;
}
.map-pin::before {
  content: "";
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: #fff;
}
.map-pin::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 100%;
  margin: -3px 0 0 -5px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 9px solid var(--brand);
  filter: drop-shadow(0 1px 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(1px 0 0 #fff);
}

/* The crosshair the map moves under. Placing a pin is "put it here" rather
   than "hit that spot", which is also the only way a keyboard can aim. */
/* The crosshair sits under the pin, not over it. Both are at the centre the
   moment somebody presses "put the pin in the middle", and a crosshair drawn
   on top hides the very thing it was used to place. */
.map-cross {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  pointer-events: none;
  border: 1px solid rgb(0 0 0 / 0.55);
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.7);
}
.map-cross::before,
.map-cross::after {
  content: "";
  position: absolute;
  background: rgb(0 0 0 / 0.55);
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.55);
}
.map-cross::before { top: 50%; left: -9px; right: -9px; height: 1px; margin-top: -0.5px; }
.map-cross::after { left: 50%; top: -9px; bottom: -9px; width: 1px; margin-left: -0.5px; }

.map-zoom {
  position: absolute;
  top: var(--s3);
  right: var(--s3);
  display: grid;
  gap: 1px;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-2);
}
.map-btn {
  width: 2.25rem;
  height: 2.25rem;
  border: 0;
  background: var(--surface);
  color: var(--ink);
  font-size: var(--step-1);
  line-height: 1;
  cursor: pointer;
}
.map-btn:hover:not(:disabled) { background: var(--quiet-soft); }
.map-btn:disabled { opacity: 0.4; cursor: default; }

/* Attribution is a condition of using the tiles, not a nicety, so it is part
   of the map rather than something a page can forget to include. */
.map-credit {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 2px var(--s2);
  font-size: 0.7rem;
  line-height: 1.6;
  color: var(--ink-2);
  background: color-mix(in oklab, var(--surface) 82%, transparent);
  border-top-left-radius: var(--radius);
}
.map-credit a { color: inherit; }

.map-actions { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center; }

.search-result { margin: 0; font-size: var(--step--1); }
.search-result.is-good { color: var(--good); }
.search-result.is-bad { color: var(--bad); }

/* The address a search starts from, and the two number fields under the map.
   A container query rather than a media query: this sits inside a card as
   often as it sits in a page, and a media query cannot tell a narrow card
   from a narrow window. */
.map-coords { display: grid; gap: var(--s3); }
.map-field:has(> .map-coords) { container-type: inline-size; }
@container (min-width: 30rem) {
  .map-coords { grid-template-columns: 1fr 1fr; }
}

/* Where a map is shown rather than chosen: on a course page, on a school's
   own page. The link beside it is what a reader gets with no script, so it is
   never hidden when the map appears. */
.place { display: grid; gap: var(--s3); }
.place-lines { margin: 0; }
.place-lines dt { font-weight: 600; }
.place-link { font-size: var(--step--1); }

@media print {
  /* A map is tiles fetched over the network. On paper it is a grey box, so
     the address and the link print instead. */
  .map { display: none; }
}

/* ------------------------------------------------- the compliance matrix --
   Colleagues down the side, required training across the top. It is the
   widest thing in the application, so it lives in a .table-wrap and scrolls
   sideways inside it: .shell uses overflow-x: clip rather than hidden
   precisely so that a scroller inside it still works.

   Every cell carries a word as well as a colour. That is not politeness: the
   evidence pack prints in black and white, and a grid whose meaning is in the
   hue says nothing on paper and nothing to a reader who cannot see it. */
.matrix { min-width: 100%; }

/* th sets white-space: nowrap for every other table in the application, where a
   header is one or two words. Here a header is a module title, and nowrap made
   the columns overlap and run off the right-hand edge of the card: the text
   simply overflowed its cell, silently, and no measurement noticed because the
   table lives in a horizontal scroller and is allowed to be wider than the card
   that holds it. */
.matrix th[scope="col"] {
  vertical-align: bottom;
  white-space: normal;
  overflow-wrap: anywhere;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--step--1);
  color: var(--ink);
  width: 11rem;
  min-width: 11rem;
}
.matrix th[scope="col"] .quiet {
  display: block;
  font-weight: 400;
  letter-spacing: 0;
  margin-top: 2px;
}
.matrix th[scope="row"] {
  text-align: left;
  font-weight: 500;
  white-space: normal;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--step--1);
  color: var(--ink);
  min-width: 13rem;
  background: transparent;
}
.matrix td { vertical-align: top; white-space: normal; }

.cell {
  display: inline-block;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-pill);
  font-size: var(--step--1);
  font-weight: 600;
  line-height: 1.35;
  border: 1px solid transparent;
}
.cell-good { background: var(--good-soft); color: var(--good); }
.cell-warn { background: var(--warn-soft); color: var(--warn); }
.cell-bad { background: var(--bad-soft); color: var(--bad); }
.cell-info { background: var(--info-soft); color: var(--info); }
/* Not required of this person. A blank cell reads as missing data, so it says
   so, quietly enough not to compete with the cells that mean something. */
.cell-none {
  color: var(--ink-3);
  font-weight: 400;
  border-color: var(--rule);
}

/* The tick boxes that record what somebody does. Laid out as a wrapping row
   rather than a column, because a person holds two roles and a stacked list of
   ten makes a table row eleven lines tall. */
.rolepicker fieldset {
  border: 0;
  margin: 0 0 var(--s2);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
}
.rolepicker .choice { margin: 0; }

.rolelist { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); }
.rolelist li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: flex-start;
  justify-content: space-between;
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--rule);
}
.rolelist li:last-child { border-bottom: 0; padding-bottom: 0; }
.rolelist .pill { margin-left: var(--s1); }

/* The situation a question is about, where it has one. A safeguarding check
   that asks what somebody would do about a disclosure needs a paragraph, and
   often a picture, and both belong inside the fieldset so a screen reader
   reaches them before the answers rather than after. */
.scenario {
  display: grid;
  gap: var(--s3);
  margin: var(--s3) 0 var(--s4);
  padding: var(--s4);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
  border-left: 3px solid var(--accent, var(--hue-a));
}
.scenario img {
  display: block;
  width: 100%;
  max-width: 28rem;
  height: auto;
  border-radius: var(--radius);
}
.scenario .prose > :last-child { margin-bottom: 0; }

/* Wide enough and the picture sits beside the words rather than above them.
   A container query, not a media query: this lives inside a question inside a
   card, and how much room it has is not what the window is doing. */
.question:has(> .scenario) { container-type: inline-size; }
@container (min-width: 34rem) {
  .scenario:has(img):has(.prose) { grid-template-columns: 16rem 1fr; align-items: start; }
}

/* ------------------------------------------------------ a report's chart --
   Server-rendered SVG, because no journey here may need JavaScript and a chart
   that appears only when a script runs is a hole in the page for everybody
   else. Every position is a geometry attribute on the element; every colour is
   one of these classes. There is no style attribute anywhere in the markup,
   because style-src names a nonce and a nonce cannot whitelist one: an inline
   style would be discarded by the browser without a word in any log.

   The decorative hues cycle by position. That is safe here, and only here,
   because a bar carries its own label and its own figure underneath: the colour
   says nothing, so nothing is lost by a reader who cannot tell them apart. */
.chart { margin: 0; }
.chart-svg {
  display: block;
  width: 100%;
  height: auto;
  max-height: 22rem;
  overflow: visible;
}
.chart-axis { stroke: var(--rule-strong); stroke-width: 1; }
.chart-bar { fill: var(--accent, var(--hue-a)); }
.chart-line { stroke: var(--hue-a); stroke-width: 2.5; }
.chart-dot { fill: var(--accent, var(--hue-a)); }
.chart-bar.hue-0, .chart-dot.hue-0 { --accent: var(--hue-a); }
.chart-bar.hue-1, .chart-dot.hue-1 { --accent: var(--hue-b); }
.chart-bar.hue-2, .chart-dot.hue-2 { --accent: var(--hue-d); }
.chart-bar.hue-3, .chart-dot.hue-3 { --accent: var(--hue-c); }
.chart-label {
  fill: var(--ink-2);
  font-size: 13px;
  font-family: inherit;
}
.chart figcaption {
  margin-top: var(--s3);
  color: var(--ink-2);
  font-size: var(--step--1);
}

/* The rows of the builder. A grid of selects rather than a stack of fieldsets,
   because twelve columns as twelve cards is a page nobody can see the shape of,
   and the shape is the thing being edited. */
.builder-grid { width: 100%; }
.builder-grid th { white-space: normal; }
.builder-grid td { padding: var(--s2) var(--s3); vertical-align: middle; }
.builder-grid select, .builder-grid input { width: 100%; min-width: 8rem; }

@media print {
  /* A chart prints. It is the one picture in this application that carries
     figures rather than decoration, so it is deliberately not hidden the way
     the map is. */
  .chart-svg { max-height: 14rem; }
}

/* ------------------------------------------------------------ help chat --
   The bubble in the corner of every signed-in page, and the pieces the help
   page shares with it.

   It is a <details> element, so opening and closing costs no JavaScript. That
   is worth stating in the stylesheet as well as in the markup, because the
   rules below are written around a <summary> being the control and not a
   button: [open] is the state, and there is no class to toggle. */

.chat-bubble {
  position: fixed;
  right: var(--s4);
  bottom: var(--s4);
  z-index: 40;
  max-width: calc(100vw - var(--s5));
}

/* The tab. A summary with the marker removed rather than list-style: none
   alone, because Safari needs the WebKit pseudo-element as well. */
.chat-bubble-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--brand);
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: var(--on-brand);
  font-weight: 600;
  font-size: var(--step--1);
  cursor: pointer;
  box-shadow: var(--shadow-2);
  list-style: none;
  width: fit-content;
}
.chat-bubble-tab::-webkit-details-marker { display: none; }
.chat-bubble-tab:hover { background: var(--brand-hover); border-color: var(--brand-hover); }
.chat-bubble-tab:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: 2px; }
.chat-bubble[open] .chat-bubble-tab { margin-bottom: var(--s3); }

.chat-bubble-icon {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linejoin: round;
}
.chat-bubble-tab .badge { background: var(--on-brand); color: var(--brand); }

.chat-bubble-panel {
  width: min(24rem, calc(100vw - var(--s5)));
  max-height: min(32rem, calc(100vh - 6rem));
  overflow-y: auto;
  padding: var(--s4);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-3);
}
.chat-bubble-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.chat-bubble-full { font-size: var(--step--1); }
.chat-bubble-greeting { margin: 0 0 var(--s4); color: var(--ink-2); }
.chat-bubble-note, .chat-bubble-foot { font-size: var(--step--1); }
.chat-bubble-foot { margin: var(--s3) 0 0; }
.chat-bubble-form .field { margin-bottom: var(--s3); }

/* Send stays on screen. The panel scrolls, and a conversation with a picture
   attached is taller than it, so without this the one control the whole thing
   exists for is below the fold of a box most people will not think to scroll. */
.chat-bubble-form .btn-group {
  position: sticky;
  bottom: calc(var(--s4) * -1);
  margin: 0;
  padding: var(--s3) 0 var(--s4);
  background: var(--surface);
}

/* Inside 24rem there is no room for the reply-width limit the full page uses,
   and a bubble is short enough that alternating sides still reads. */
.thread-tight li { max-width: 100%; padding: var(--s3) var(--s4); }
.chat-bubble-thread { margin-bottom: var(--s3); }

/* The picture: the file input, the two things the script adds, and the
   preview. The preview is required rather than decorative, because a browser
   capture can include whatever window the person picked in their own dialog
   and they have to see it before it goes. */
.chat-shot {
  margin-bottom: var(--s4);
  padding: var(--s3);
  border: 1px dashed var(--field-line);
  border-radius: var(--radius);
}
.chat-shot .field-tight { margin-bottom: var(--s2); }
.chat-shot-preview {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  margin: var(--s3) 0 0;
}
.chat-shot-preview img {
  max-width: 10rem;
  max-height: 7rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.chat-attachment {
  max-width: 100%;
  max-height: 18rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

@media print {
  /* A page printed for a governors' meeting must not carry the school's
     conversation with the council down the corner of it. */
  .chat-bubble { display: none !important; }
}

/* -------------------------------------------------- the council's console --
   People on the left, the conversation in the middle, everything about them on
   the right.

   This is a media query and not a container query, legitimately: it is the
   top-level layout of a page and it genuinely is about the window. The rule
   about container queries applies to what sits inside a card, which cannot
   tell a narrow card from a narrow window -- and the cards in the right-hand
   column here are exactly that case, which is why .deflist already asks one. */

.chat-console {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s5);
  align-items: start;
}

@media (min-width: 1100px) {
  .chat-console { grid-template-columns: 20rem minmax(0, 1fr); }
  .chat-console.has-open { grid-template-columns: 19rem minmax(0, 1fr) 20rem; }
}

/* Below the breakpoint the two are one page each: the list, or the
   conversation with a way back to it. Stacking forty people above the
   conversation would mean scrolling past all of them to read a reply. */
@media (max-width: 1099px) {
  .chat-console.has-open .chat-list { display: none; }
}
@media (min-width: 1100px) {
  .chat-back { display: none; }
}
.chat-back { margin: 0 0 var(--s4); }

.chat-list { position: sticky; top: calc(var(--header-h) + var(--s3)); }
@media (min-width: 1100px) {
  .chat-list { max-height: calc(100vh - var(--header-h) - var(--s6)); overflow-y: auto; }
}

.chat-filters { margin-bottom: var(--s4); }
.chat-filters .field-tight { margin-bottom: var(--s2); }
.chat-filters select, .chat-filters input { width: 100%; }

.chat-people { list-style: none; margin: 0; padding: 0; }
.chat-people li + li { border-top: 1px solid var(--rule); }

.chat-person {
  display: grid;
  gap: var(--s1);
  padding: var(--s3) var(--s2);
  color: inherit;
  text-decoration: none;
  border-left: 3px solid transparent;
}
.chat-person:hover { background: var(--tint); }
.chat-person.is-open { border-left-color: var(--brand); background: var(--tint); }
.chat-person:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: -3px; }

.chat-person-top { display: flex; align-items: center; gap: var(--s2); }
.chat-person-name { font-weight: 600; }
.chat-person-where { font-size: var(--step--1); color: var(--ink-3); }
.chat-person-line {
  font-size: var(--step--1);
  color: var(--ink-2);
  /* A line of somebody's message, on one line. Without this a long first
     sentence turns every row into a paragraph and the list stops being
     scannable, which is the only thing the left column is for. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chat-person-foot {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  font-size: var(--step--1);
}

.chat-side { display: grid; gap: var(--s4); }
.chat-side .card + .card { margin-top: 0; }
.chat-side h3 { margin-top: var(--s5); }

.chat-saved { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s2) 0 0; padding: 0; }

.chat-notes { list-style: none; margin: var(--s4) 0 0; padding: 0; }
.chat-notes li {
  padding: var(--s3);
  border-left: 3px solid var(--warn);
  background: var(--warn-soft);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.chat-notes li + li { margin-top: var(--s2); }


/* --------------------------------------------------------------- the guide --

   Sixty pages down the left, whichever one is open on the right. The shape is
   the chat console's, deliberately: a sticky capped scroller beside a column of
   content is a pattern this application already has, and a manual that looks
   like a document dropped into the middle of the site reads as somebody else's
   website.

   The index is a <nav> with no heading in it. Its group labels are paragraphs
   naming their own lists, so the page's h1 is the first heading on the page and
   the body's h2s follow it. */

.manual {
  display: grid;
  gap: var(--s5);
  grid-template-columns: 1fr;
  align-items: start;
  padding-block: var(--s3) var(--s6);
}
@media (min-width: 1100px) {
  .manual { grid-template-columns: 19rem minmax(0, 1fr); gap: var(--s6); }
}

/* Below the breakpoint the two are one page each: the whole index, or one page
   with a way back to it. Stacking sixty links above the content would mean
   scrolling past all of them to read the answer, which is the same argument the
   chat console makes about forty people above a conversation. */
@media (max-width: 1099px) {
  .manual.has-open .manual-index { display: none; }

  /* On the front page at this width the index would be a 2,500px wall of sixty
     links standing between the reader and the same sixty links again, with
     their summaries, as cards below it. The cards are the better index, so what
     stays here is the search box and nothing else. Measured at 375px: the
     alternative was 2,575px of navigation in an 812px window. */
  .manual-index .manual-group,
  .manual-index .manual-links,
  .manual-index .manual-clear { display: none; }
  .manual-index { position: static; }
  .manual-find { margin-bottom: 0; }
}
@media (min-width: 1100px) {
  .manual-back { display: none; }
}
.manual-back { margin: 0 0 var(--s4); }

.manual-index { position: sticky; top: calc(var(--header-h) + var(--s3)); }
@media (min-width: 1100px) {
  .manual-index {
    max-height: calc(100vh - var(--header-h) - var(--s6));
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

.manual-find { display: flex; gap: var(--s2); margin-bottom: var(--s4); }
.manual-find input { flex: 1 1 auto; min-width: 0; }

.manual-group {
  margin: var(--s5) 0 var(--s2);
  font-size: 0.74rem;
  font-weight: 660;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.manual-group:first-of-type { margin-top: 0; }

.manual-links { list-style: none; margin: 0; padding: 0; }

.manual-link {
  display: block;
  padding: var(--s2) var(--s3);
  border-left: 3px solid transparent;
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--ink-2);
  text-decoration: none;
  font-size: var(--step--1);
  line-height: 1.35;
}
.manual-link:hover { background: var(--tint); color: var(--ink); }
.manual-link:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: -3px; }
/* Current by a rule and a weight as well as a tint: colour alone says nothing
   to a reader who cannot see it, and aria-current says the rest. */
.manual-link.here {
  border-left-color: var(--brand);
  background: var(--tint);
  color: var(--brand-ink);
  font-weight: 620;
}

.manual-clear { margin: var(--s4) 0 0; font-size: var(--step--1); }

.manual-main { min-width: 0; }
/* The front page follows its lede with a sentence about what this is not, and
   .lede has no bottom margin of its own because on every other page it is the
   last thing in the head. */
.manual-main .page-head .lede + p { margin-top: var(--s4); }
.manual-who { margin-bottom: var(--s4); color: var(--ink-2); }

.manual-contents {
  margin-bottom: var(--s5);
  padding: var(--s4) var(--s5);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
}
.manual-contents-title {
  margin: 0 0 var(--s2);
  font-size: 0.74rem;
  font-weight: 660;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.manual-contents ul { columns: 2; column-gap: var(--s6); }
@media (max-width: 640px) { .manual-contents ul { columns: 1; } }
.manual-contents li { margin-bottom: var(--s1); break-inside: avoid; }

/* A guide page is prose with tables and lists in it, so it takes the reading
   measure that every other long-form body here takes, and gives it back to
   anything that has to be wider than a column of text. */
.manual-prose > .table-wrap { max-width: none; }

.manual-nextprev {
  display: grid;
  gap: var(--s3);
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
}
@media (min-width: 700px) {
  .manual-nextprev { grid-template-columns: 1fr 1fr; }
}
.manual-step {
  display: grid;
  gap: var(--s1);
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
}
.manual-step:hover { border-color: var(--brand); background: var(--tint); }
.manual-step:focus-visible { outline: 3px solid var(--accent-ring); outline-offset: 2px; }
/* Next sits on the right of the pair even when there is no previous, so the
   two never swap sides between one page and the next. */
.manual-step-next { grid-column: -2; text-align: right; }

.manual-ask { margin-block: var(--s5); }

/* Printed for a staff meeting, a guide page is the page. The index, the search
   and the invitation to start a conversation are all screen furniture. */
@media print {
  .manual { display: block; }
  .manual-index,
  .manual-find,
  .manual-back,
  .manual-ask,
  .manual-nextprev { display: none; }
}
