/* ── Self-hosted brand faces ──────────────────────────────────────────
   Montserrat is the logo face and carries headings and UI; Karla is the
   body face. There is deliberately no third family — micro-labels are
   Karla 700 with tracking (.fc-label).

   Self-hosted, not CDN-loaded: this app holds clinical records, so a
   runtime request to a third-party font host is a privacy property, not
   just a performance one. Files and unicode-ranges come from
   @fontsource-variable; licences sit beside the woff2 in static/fonts/. */
@font-face {
  font-family: 'Montserrat Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("../fonts/montserrat-latin-ext-wght-normal.d16dfe26bcd7.woff2") format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Montserrat Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("../fonts/montserrat-latin-wght-normal.311d352d9323.woff2") format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
  font-family: 'Karla Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 200 800;
  src: url("../fonts/karla-latin-ext-wght-normal.b8948c38fcee.woff2") format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Karla Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 200 800;
  src: url("../fonts/karla-latin-wght-normal.3006def87dd6.woff2") format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

/* ── Design tokens ─────────────────────────────────────────────────────
   Two layers. BRAND CONSTANTS are the eight colours from the 2026 brand
   guidelines (06.08.2026) plus three derived surfaces; they never change
   between themes. ROLE TOKENS are what the rest of this file actually
   uses, and they are repointed per theme below.

   The palette is FLAT for surfaces — ink, ink-raised, paper, white, line
   and nothing in between. Do not introduce a tint of a brand hue as a
   surface; use color-mix() for a wash.

   Colours that ENCODE something (the module key, the calendar event
   types) are the one exception: they carry a per-surface step, because
   no single flat set clears contrast on both paper and ink. On paper
   only ink, grey and purple are text-safe; on ink, purple fails even the
   3:1 graphical threshold. See BRAND.md in this repo for the full
   reasoning and the reconciliation with the marketing BRAND.md. */
:root {
  /* Brand constants */
  --ink: #1d2238;         /* FC black  — dark surface, body text on light */
  --ink-raised: #252b45;  /* a panel sitting on ink */
  --paper: #eff1f5;       /* FC white  — page background, not pure white */
  --green: #00fda7;       /* FC green  — a FILL, never text on light */
  --grey: #5e616d;        /* FC grey   — secondary text on light */
  --on-ink: #c9cdda;      /* body copy on ink */
  --on-ink-soft: #a7acbe; /* labels on ink */

  /* Module key, rendered on the ink shell. Constant in both themes,
     because the shell is always ink. Identity is fixed: blue = bookings,
     purple = notes, red = contracts, yellow = payments. */
  --shell-key-blue: #009fff;   /* pure brand */
  --shell-key-purple: #de62d7; /* brand #9b2094 is 2.3:1 on ink — lifted */
  --shell-key-red: #ff5288;    /* brand #ff286c is 4.3:1 on ink — lifted */
  --shell-key-yellow: #ffb121; /* pure brand */

  /* Module key, rendered in the content area. Light steps here; the dark
     theme points these at the shell steps below. */
  --key-blue: #0072b8;
  --key-purple: #9a2093;
  --key-red: #db0045;
  --key-yellow: #986200;

  /* The accent: "ink carries, green lights" (UI refresh, 2026-09-25).
     A primary action on the light content area is a navy fill with a
     neon green label, the same pairing as the sidebar, the logo tile and
     the marketing site's main call to action, so the app has one brand
     colour. Purple is now a data colour only (supervision, see --cal-*).
     Green on ink is 11.66:1; ink on paper is 13.88:1. */
  --accent: var(--ink);
  --on-accent: var(--green);
  --accent-hover: var(--ink-raised);

  /* The done / on-track mark: ticks, progress bars, the setup ring, "Paid"
     and "Confirmed" labels, the chart line, the heading full stop. Ink on
     the light content area, neon green once the content area is ink (dark
     block). Charlotte's palette (#301) retired the dark greens this used to
     be (#006b47 text, #007e53 lines): they read as another brand's green,
     and green on white was only ever a stepped-down compromise. Green is
     now never text or a line on a light surface, only a fill behind ink, a
     pale tint, or decoration. */
  --ok-fg: var(--ink);

  /* Links: ink text with a 2px neon underline at rest, growing to a 6px
     neon band on hover (#301). The underline is decoration under ink text,
     the one way neon may sit on paper; it is not a measured mark (1.35:1),
     so links are told apart by being underlined at all. */
  --link-line: var(--green);
  --link-hover: var(--green);

  /* Selected, success and hover washes, always under ink or --ok-fg. Also
     the graphs' pale mint shading (option B, #301). */
  --select-wash: color-mix(in srgb, var(--green) 22%, transparent);

  /* Boundaries. --line-strong edges secondary buttons and segmented
     controls, whose labels already identify them; --field-border edges
     inputs, which need 3:1 on their own (SC 1.4.11): 3.17:1 on white. */
  --line-strong: #c5cad8;
  --field-border: #8a90a6;

  /* A surface one step off the card, for secondary hovers and neutral pills. */
  --raised: var(--paper);

  /* The label on a --danger-fg fill: white in light (5.90:1), ink in dark,
     where white on the lifted red is only 3.08:1. */
  --on-danger: #ffffff;

  /* Focus ring (brief §2.3). A box-shadow, not a colour: a gap in the
     surface colour, then ink, then green, so it reads on paper, cards and
     the ink shell alike. Built from three colour tokens so each scope that
     changes surface (the shell, dialogs) can restate the gap and colours;
     --focus-ring itself is re-declared in those scopes (see the rule after
     the dark block) because a custom property resolves its var()s where
     it is declared, not where it is used. */
  --ring-gap: var(--paper);
  --ring-inner: var(--ink);
  --ring-outer: var(--green);

  /* Radii. Absolute px, chosen per element size so nothing becomes a pill
     by accident. NOT derived from the logo tile, which rounds at 12%. See
     the marketing repo's BRAND.md, the canonical brand spec. Only the
     status pill (.pill, UI refresh §2.4) and the calendar toolbar's Full
     day/Full week zoom toggles (issue #97, a deliberate on/off-switch
     shape) are allowed a 999px radius;
     test_only_the_named_components_are_pills pins that list so it cannot
     spread unnoticed. */
  --radius-sq: 2px;
  --radius-btn: 4px;
  --radius-card: 8px;
  --radius-panel: 14px;

  /* Roles — light theme */
  --fg: var(--ink);
  /* A step down from --fg, for text that should read as lower emphasis
     without dropping to --muted (dark brief §4.1). Currently only the
     neutral pill's label (`.pill-neutral`); clears 4.5:1 on --raised in
     both themes (ROLE_CONTRACTS in apps/core/tests/test_brand.py). */
  --fg-soft: #3a3f55;
  --muted: var(--grey);
  --line: #dcdfe8;
  --bg: var(--paper);
  --card: #ffffff;
  /* Status levels, Charlotte's palette (#301): blue informs, red asks
     for attention. Blue is for information and nothing else. Each colour
     is the brand hue stepped until it is text-safe on light (#009fff ->
     #0068a7, #ff286c -> #c90040), and one rule governs both surfaces:

       on a PLAIN surface the status colour may be text;
       on a WASH OF ITSELF the copy is --fg and the colour is the stripe.

     Amber went with #301. A heads-up ("Owed", "expires in 30 days") is
     the same red as an error but drawn as an OUTLINE on the plain card,
     with no wash, so it never looks like "Overdue" (a pink wash): --warn-fg
     is the red, --warn-bg is the card itself, and the stripe on a
     heads-up box is --danger-accent.

     The danger wash is 8%, not 12%: #c90040 on a 12% wash of itself over
     paper is 4.21:1, under AA, and the "Overdue" pill puts it there. */
  --info-fg: #0068a7;
  --danger-fg: #c90040;
  --warn-fg: var(--danger-fg);
  --info-bg: color-mix(in srgb, var(--info-fg) 12%, transparent);
  --warn-bg: var(--card);
  --danger-bg: color-mix(in srgb, var(--danger-fg) 8%, transparent);

  /* Darkened hover for the filled danger button (brief section 2.1): 85%
     --danger-fg mixed with --ink (#af053f, 7.20:1 under white). A
     pre-computed literal, not a live color-mix(), because the dark
     override below needs a DIFFERENT percentage: the dark theme's --on-danger is literally --ink, so
     mixing --danger-fg toward --ink pulls the hover fill toward the same
     colour as its own label as the percentage rises. 85% only reaches
     4.01:1 there. See the danger-hover rows in ROLE_CONTRACTS. */
  --danger-hover: #af053f;

  /* The literal brand red, for the error stripe and fill in BOTH themes.
     It clears 3:1 on all four surfaces (3.22 paper, 3.64 card, 4.31 ink,
     3.82 ink-raised) so it can be a marker anywhere, and it is under
     4.5:1 on light, so it must never carry text. The brand BLUE gets no
     equivalent: #009fff is 2.51:1 on paper, under even the graphical
     minimum, so light info uses the stepped #0068a7 above. */
  --danger-accent: #ff286c;

  /* The app shell is the ink surface in BOTH themes; only the content
     area follows data-theme. That is what lets FC green work as the
     active-state accent regardless of preference, and it means exactly
     one logo variant and one strapline-mask colour are ever needed. */
  --shell-bg: var(--ink);
  --shell-fg: var(--on-ink);
  --shell-fg-soft: var(--on-ink-soft);
  --shell-line: #363c5e;
  --shell-accent: var(--green);
  /* The lifted green the dark theme hovers a green fill to. The shell is
     ink in both themes, so its one green fill (the pilot's Feedback
     button) hovers the same way in both. */
  --shell-accent-hover: #5dffc6;
}

/* Dark theme — set via data-theme on <html> (base.html / app_base.html),
   resolved from CounsellorProfile.theme_preference by
   apps.core.context_processors._theme_for. The content area becomes the
   ink surface; the shell tokens above are untouched. */
:root[data-theme="dark"] {
  --fg: var(--on-ink);
  --fg-soft: var(--on-ink);
  --muted: var(--on-ink-soft);
  --line: #363c5e;
  --bg: var(--ink);
  --card: var(--ink-raised);
  --info-fg: #009fff;
  --danger-fg: #ff5288;
  --warn-fg: var(--danger-fg);
  --warn-bg: var(--card);
  --danger-hover: #f45084; /* 95%, not 85% - see the light block's comment */
  --key-blue: var(--shell-key-blue);
  --key-purple: var(--shell-key-purple);
  --key-red: var(--shell-key-red);
  --key-yellow: var(--shell-key-yellow);
  --accent: var(--green);
  --on-accent: var(--ink);
  --accent-hover: #5dffc6;
  --ok-fg: var(--green);
  --link-line: var(--green);
  --link-hover: color-mix(in srgb, var(--green) 38%, transparent);
  --select-wash: color-mix(in srgb, var(--green) 14%, transparent);
  --line-strong: #454c74;
  --field-border: #737ba3;
  /* One step lighter than the dark card. D2 moves the card to ink and this
     to ink-raised; until then it has to clear the current ink-raised card. */
  --raised: #2f3556;
  --on-danger: var(--ink);
  --ring-gap: var(--ink);
  --ring-inner: var(--green);
  --ring-outer: transparent;
}

/* --focus-ring re-declared wherever the surface under a focused control
   changes, so --ring-gap and the ring colours resolve for that scope.
   body > header and the app shell are ink in both themes; dialogs sit on
   --card. Each scope sets its ring tokens beside its other rules. */
:root,
dialog,
body > header,
.app-shell-sidebar,
.app-shell-topbar,
.toast {
  --focus-ring: 0 0 0 2px var(--ring-gap), 0 0 0 4px var(--ring-inner), 0 0 0 7px var(--ring-outer);
}

dialog { --ring-gap: var(--card); }

* { box-sizing: border-box; }

/* Any author display rule (.btn's inline-flex, a list row's flex...)
   beats the browser's own [hidden] { display: none }, so a script
   setting el.hidden would silently do nothing on those elements. One
   rule here makes the attribute always hide (#359). Scripts must unhide
   an element before measuring, scrolling or focusing inside it. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: 'Karla Variable', ui-sans-serif, system-ui, -apple-system,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-weight: 500; /* Karla Medium — the brand's secondary weight */
  color: var(--fg);
  background: var(--bg);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* Type scale. Montserrat, sentence case; the terminal full stop is the
   one flourish, painted with the green step for light surfaces, FC green
   once the dark theme puts the content area on ink (see .fc-mark below
   and the terminal_mark filter in apps/core/templatetags/brand.py). */
h1, h2, h3, .fc-h1, .fc-h2, .fc-h3 {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, -apple-system,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
h1, .fc-h1 {
  font-size: clamp(1.6rem, 3.2vw, 2.1rem);
  font-weight: 600; /* Montserrat SemiBold — the brand's main weight */
  letter-spacing: -0.024em;
  line-height: 1.12;
  margin: 0 0 0.5rem;
}
h2, .fc-h2 {
  font-size: 1.25rem;
  font-weight: 600; /* Montserrat SemiBold — the brand's main weight */
  letter-spacing: -0.016em;
  line-height: 1.2;
}
h3, .fc-h3 {
  font-weight: 600;
  letter-spacing: -0.008em;
  line-height: 1.3;
}
.fc-lede {
  color: var(--muted);
  font-size: 1.02rem;
  line-height: 1.65;
  max-width: 60ch;
}

/* The terminal mark: --ok-fg, ink on light and --green once the dark
   theme puts the content area on ink (11.66:1 there). It was raw --green
   on both (1.19:1 on paper, invisible), then the dark green #301 retired. */
.fc-mark { color: var(--ok-fg); }

/* Micro-label: distinguished by weight and tracking, never by
   uppercasing. Lowercase is the wordmark's register and is kept for
   labels only — headings are sentence case. */
.fc-label {
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  line-height: 1.4;
  text-transform: none;
}

/* ── THE SQUARE — the platform's state marker ──────────────────────────
   Geometry is taken from the supplied Sq_*.svg bullets: a 391.89 unit
   square with a 13.11 corner radius and a frame 80.75 units thick
   (20.6% of the side), so it scales from an 11px label bullet to a 20px
   checkbox and stays on-brand.

     OPEN   = outstanding, still on the counsellor
     FILLED = done, or handled by the platform

   Both states must remain legible on the page — don't fill a square just
   because it looks tidier. Colour is inherited, so set it with color:. */
.fc-sq {
  display: inline-block;
  flex: none;
  width: var(--sq, 14px);
  height: var(--sq, 14px);
  border: calc(var(--sq, 14px) * 0.206) solid currentColor;
  border-radius: var(--radius-sq);
  background: transparent;
  transition: background-color 0.45s ease;
}

.fc-sq[data-state="filled"] { background: currentColor; }

@media (prefers-reduced-motion: reduce) {
  .fc-sq { transition: none; }
}

/* Module key. Identity is fixed (blue = bookings, purple = notes,
   red = contracts, yellow = payments); the value is that key's rendered
   step for the surface it is on. */
.fc-key-blue { color: var(--key-blue); }
.fc-key-purple { color: var(--key-purple); }
.fc-key-red { color: var(--key-red); }
.fc-key-yellow { color: var(--key-yellow); }

/* The shared page header (templates/_page_header.html): the h1, a short
   description, then breadcrumbs on a sub-page. */
.fc-page-header { margin-bottom: 1.5rem; }

/* Breadcrumbs sit BELOW the description, which is the order the founders
   asked for: the page names itself before it says where it is. The
   separator is a ::before rather than markup, so a screen reader gets an
   ordered list and the aria-current leaf, not a run of stray characters. */
.fc-breadcrumbs {
  margin-top: 1rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.fc-breadcrumbs ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
}

.fc-breadcrumbs li {
  display: flex;
  align-items: center;
  gap: 0.45rem;
}

.fc-breadcrumbs li + li::before {
  content: "\203a";
  color: var(--muted);
}

/* Buttons and nav are Montserrat too — they are UI, not body copy. */
button, .btn, .quick-links a, .app-shell-nav-link {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
}

/* Keyboard focus only (brief §2.3). The ring is a box-shadow so it can
   be a double ring; the transparent outline is what Windows High Contrast
   paints instead, since forced-colours mode drops box-shadow. */
:focus-visible {
  outline: 2px solid transparent;
  outline-offset: 2px;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sq);
}

/* A dialog heading takes focus on open (static/js/dialogs.js) so nothing
   inside looks pre-selected. It is a landmark for the screen reader, not
   a control, so it never shows a ring even after a keyboard open. */
[data-dialog-heading]:focus,
[data-dialog-heading]:focus-visible {
  outline: none;
  box-shadow: none;
}

/* Links (brief §2.2). Ink text with a 2px --link-line underline, drawn as
   a background gradient so its thickness animates and it follows wrapped
   lines, growing on hover to a 6px --link-hover band that sits behind the
   ink text. Scoped to unclassed anchors plus an explicit .link: every
   classed anchor in the app is chrome, a button or a card, and a
   background underline (unlike text-decoration) is not switched off by
   their `text-decoration: none`. A classed anchor that is a prose link
   adds `link` to its classes. */
a:not([class]),
.link {
  color: var(--fg);
  text-decoration: none;
  background: linear-gradient(var(--link-line), var(--link-line)) 0 100% / 100% 2px no-repeat;
  padding-bottom: 2px;
  transition: background-size 0.15s ease;
}

a:not([class]):hover,
.link:hover {
  background-image: linear-gradient(var(--link-hover), var(--link-hover));
  background-size: 100% 6px;
}

a:not([class]):focus-visible,
.link:focus-visible { border-radius: 2px; }

@media (prefers-reduced-motion: reduce) {
  a:not([class]),
  .link { transition: none; }
}

/* Classed anchors (chrome, buttons, cards) still default to the text
   colour rather than the browser's link blue, as the old bare `a` rule
   gave them, AND lose the browser's default underline: a class with no
   text-decoration of its own (a.btn-secondary used without .btn) otherwise
   keeps a stray UA underline now that the gradient rule above no longer
   matches it. :where() keeps both at zero
   specificity so every class rule (.btn's --on-accent, the shell's
   --shell-fg, .link-button's own underline) still wins. This makes a
   classed anchor deterministic: underlined only when it is unclassed or
   carries .link. */
:where(a) {
  color: var(--fg);
  text-decoration: none;
}

/* ---- Chrome ------------------------------------------------------------ */

/* base.html's header is chrome, not content, so — like the app shell —
   it stays the ink surface in both themes; see the app-shell rules below
   for the fuller rationale. Scoped to the direct child of <body> so this
   never matches content headers nested in <main> (e.g. _page_header.html's
   .fc-page-header masthead) or the app shell's own .app-shell-topbar. */
body > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 2rem;
  background: var(--shell-bg);
  color: var(--shell-fg);
  border-bottom: 1px solid var(--shell-line);
  /* Ink in both themes: an ink ring would vanish, so the ring is green
     on an ink gap (11.66:1), as in the dark theme. */
  --ring-gap: var(--shell-bg);
  --ring-inner: var(--green);
  --ring-outer: transparent;
}

/* Scoped to the chrome bar, for the same reason `header` is (see the
   rule above and test_chrome_header_selector_is_scoped): a bare `nav`
   selector matches every <nav> in the document, and since issue #83
   that includes .fc-breadcrumbs, nested in <main> on the light content
   area. Unscoped, breadcrumb links took --shell-fg (which is --on-ink,
   near-white) on paper and were invisible.

   `:not(.link)` (final fix wave, review finding): specificity, not just
   scope. The prose rule's `a:not([class])` is (0,1,1) — one pseudo-class
   argument, one type — which beats this rule's un-suffixed (0,0,4), so
   base.html's unclassed "Dashboard", "Log in" and "Sign up" links took
   `color: var(--fg)` (ink) on the ink header: ink on ink, with only the
   green underline showing. `:not(.link)` adds a pseudo-class argument,
   lifting this to (0,1,4), which wins on the tie-break third digit once
   the first two are equal — see test_prose_links_have_a_gradient_
   underline_and_chrome_links_do_not's header assertions and
   test_shell_nav_weight_stays_clear_of_the_body_weight, updated to match.
   `background: none` kills the prose rule's gradient-underline
   background-image too: raising specificity alone only wins the `color`
   property fight, and background is a separate declaration the old rule
   never set, so the gradient kept painting underneath regardless. */
body > header nav a:not(.link) {
  color: var(--shell-fg);
  text-decoration: none;
  font-weight: 600;
  margin-left: 1.25rem;
  background: none;
}

body > header nav .user-email {
  color: var(--shell-fg-soft);
  font-size: 0.9rem;
  margin-left: 1.25rem;
}

/* Log out in the chrome bar is a POST form (allauth signs out on POST
   only), so it has to sit inline among the bar's anchors and read as
   one of them rather than as the global green pill `button` rule
   (issue #55; the sidebar's equivalent is further down). */
body > header nav .chrome-logout {
  display: inline-block;
  margin-left: 1.25rem;
}

body > header nav .chrome-logout button {
  padding: 0;
  background: none;
  border: 0;
  color: var(--shell-fg);
  font-size: 1rem;
  font-weight: 600;
}

body > header nav .chrome-logout button:hover {
  background: none;
  text-decoration: underline;
  text-decoration-color: var(--shell-accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

main {
  max-width: 720px;
  margin: 2rem auto 4rem;
  padding: 0 1rem;
}

.messages {
  list-style: none;
  padding: 0;
  margin: 0 0 1.25rem;
}

.messages li {
  /* Two-step wash: a faint fill plus a visibly stronger border step, or
     the border disappears into its own fill. The border step is
     --ok-fg, not raw --green (Task 3): a 40% mix of the raw token is
     still neon on a light surface. */
  background: color-mix(in srgb, var(--green) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--ok-fg) 40%, transparent);
  color: var(--fg);
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius-card);
  margin-bottom: 0.5rem;
  font-size: 0.92rem;
}

/* Message levels. The bare .messages li above is the success default;
   these three repaint it. Copy stays --fg on every level - the wash is
   the signal, not the text colour - and the left stripe carries the
   level at a glance. */
.messages li.info {
  background: var(--info-bg);
  border-color: color-mix(in srgb, var(--info-fg) 40%, transparent);
  border-left: 3px solid var(--info-fg);
}
.messages li.warning {
  background: var(--warn-bg);
  border-color: var(--line);
  border-left: 3px solid var(--danger-accent);
}
.messages li.error {
  background: var(--danger-bg);
  border-color: color-mix(in srgb, var(--danger-accent) 40%, transparent);
  border-left: 3px solid var(--danger-accent);
}

/* ---- Notes (Task 3, brief §2.7) -----------------------------------------
   An aside in ink on a tint. A coloured stripe marks only a warning or a
   danger; information is not an alarm, so .note and .note-info carry no
   stripe at all — a change from the blue left-bar callouts (.safeguarding-
   note, .hold-expiry, .milestone-alert-info,
   .widget-attention's border) they replace.

   Each kind gets its own standalone block, repeating the shared padding,
   radius and text colour, rather than one grouped selector list: base.css
   tests read a rule by searching for "<selector> {", and a grouped list
   like ".note, .note-info { ... }" never contains ".note {" on its own.

   .note carries a 1px --line-strong border, the kinds do not (Task 3
   review, fix 1): --raised equals --paper in light theme, so a neutral
   note sitting directly on the page (safeguarding.html) was invisible -
   no wash of its own to separate it from the surface behind it. The info/
   warn/danger kinds don't need this: their background is a status wash
   that already reads against both --paper and --card. */
.note {
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-card);
  color: var(--fg);
  background: var(--raised);
  border: 1px solid var(--line-strong);
}

.note-info {
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-card);
  color: var(--fg);
  background: var(--info-bg);
}

/* A heads-up (#301, reminder banner option A): the plain card with a
   --line edge and the brand-red stripe. No wash, so it never reads as the
   pink of .note-danger. */
.note-warn {
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-card);
  color: var(--fg);
  background: var(--warn-bg);
  border: 1px solid var(--line);
  border-left: 3px solid var(--danger-accent);
}

.note-danger {
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-card);
  color: var(--fg);
  background: var(--danger-bg);
  border-left: 3px solid var(--danger-accent);
}

code {
  background: var(--line);
  padding: 0 0.25rem;
  border-radius: 3px;
}

/* ---- Forms ------------------------------------------------------------- */

form p,
form .form-row { margin: 0 0 0.85rem; }

form p label,
form .form-row > label:not(.check-row) {
  display: block;
  font-size: 0.844rem; /* 13.5px */
  font-weight: 700;
  margin-bottom: 0.3rem;
}

/* An annotated row puts the label and its "i" on one line, so the label
   shrink-wraps instead of taking the full width. :not(.check-row) so this
   never touches a check-row label: without it, this rule's specificity
   (a .form-row, a :has(), and a label type selector) still out-ranks
   .check-row's own single class, silently setting display: inline-block on
   a checkbox's wrapping label wherever it also carries a "why" disclosure
   (Task 5 review: onboarding requirements, whose four checkboxes all have
   `why` text). */
form .form-row:has(.field-why) > label:not(.check-row) {
  display: inline-block;
  margin-bottom: 0;
}

/* Keep a trailing, unwrapped checkbox label on one line with its box, in
   the as_p fallback only. _field_row.html and _annotated_fields.html
   always wrap a checkbox in label.check-row now (Task 5), so the
   `.form-row` variant this rule used to pair with is dead template-side -
   worse than dead, actually: it matched label.check-row too (a `.form-row`
   direct child `<label>`), and its specificity (a `form`, a `.form-row`, a
   `:has(input[type="checkbox"])`, and a `label` type selector) beat
   .check-row's own single class outright, silently collapsing the
   checkbox-leads-label layout back to `display: inline` wherever a
   `.form-row` checkbox rendered (Task 5 review: onboarding requirements,
   booking's require_card, practice's vat_registered). Dropped rather than
   given a :not(.check-row) guard, since as_p rendering is the only place
   left with a bare, unwrapped checkbox <label>. */
form p:has(input[type="checkbox"]) label { display: inline; }

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="tel"],
input[type="url"],
input[type="search"],
select,
textarea {
  font: inherit;
  color: inherit;
  width: 100%;
  max-width: 26rem;
  padding: 0.45rem 0.6rem;
  background: var(--card);
  border: 1px solid var(--field-border);
  border-radius: var(--radius-btn);
}

/* The ring (:focus-visible) marks keyboard focus; the border darkening to
   the text colour marks the active field for mouse users too. */
input:focus, select:focus, textarea:focus { border-color: var(--fg); }

.helptext {
  display: block;
  color: var(--muted);
  font-size: 0.781rem; /* 12.5px */
  margin-top: 0.25rem;
  max-width: 26rem;
}

input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--accent);
  vertical-align: middle;
}

/* A checkbox leads its label, and the whole label is the hit area. */
.check-row {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.6rem;
  font-weight: 600;
  cursor: pointer;
}

/* The "+" beside a SelectWithAddNew (issue #37). Secondary-button treatment
   per BRAND.md, matching button[name="action"][value="add"]: a green fill
   here would read as the form's primary action, which an escape hatch beside
   a field is not. text-decoration is killed because it is a link, and links
   on light surfaces carry an accent underline. */
.select-with-add {
  display: inline-flex;
  align-items: stretch;
  gap: 0.4rem;
}

/* No height of its own: stretching to the row means it always matches the
   select beside it, whatever padding the field rule above carries. */
.add-new-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  padding: 0 0.6rem;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius-btn);
  font-weight: 600;
  font-size: 1.1rem;
  line-height: 1;
  text-decoration: none;
}

.add-new-button:hover { background: var(--raised); }

.errorlist {
  list-style: none;
  padding: 0;
  margin: 0 0 0.25rem;
  color: var(--danger-fg);
  font-size: 0.85rem;
}

/* In a form row the errors follow the input (app#689): space them off it. */
.form-row > .errorlist { margin: 0.25rem 0 0; }

/* Required-field star. A form opts in via Django's required_css_class
   (currently only the About You step's name fields, issue #44); other
   wizard steps deliberately carry no stars. */
.form-row.required > label::after { content: " *"; color: var(--danger-fg); }

/* The settings page groups its fields into fieldsets (issue #97): Your
   details / Your working week / Journal. forms/_working_week.html also
   wraps its per-day rows in their own (nested) fieldset, so this rule is
   deliberately a descendant selector rather than a direct-child one,
   giving that fieldset the same boxed treatment as its parents. */
.settings-form fieldset {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1rem;
  margin: 0 0 1rem;
}

/* The settings page's fieldset legends. The entry dialog's payment
   fieldset used to share this; since phase 5 task 2 it is one of the
   client session form's sections (.form-section > legend, below). */
.settings-form legend {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, -apple-system,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-weight: 600;
  padding: 0 0.3rem;
}

/* ---- "Why we ask" field disclosures ------------------------------------ */

/* The closed icon shares the label's line; the field keeps the line below it.
   `display: inline` buys both: the summary is an inline box, so it sits beside
   the label, while the block paragraph inside splits that inline box (CSS 2.1
   block-in-inline), landing the callout at the row's left edge — flush with
   every other control — and pushing the field down rather than shouldering it
   aside. The empty `::after` block makes the same split happen while the
   disclosure is shut, so the field's left edge never moves, in any engine.

   Inline-block (the first attempt) does not work: the field then shares the
   label's line, and an open callout is narrow enough to sit beside the label
   rather than wrap under it, which drags the label down to the callout's last
   baseline and throws the field out to the right of it. */
.field-why { display: inline; }

.field-why::after { content: ""; display: block; }

/* Checkbox rows are the exception: label.check-row (Task 5) already wraps
   the box and its text in one inline-flex label, and this disclosure is
   its next sibling, so a closed disclosure only needs to be inline-level
   itself to share that line - it targets .field-why, not label, so it
   never competes with .check-row's own display: inline-flex above.
   Inline-block contains the split until the student actually opens the
   callout. */
form .form-row:has(input[type="checkbox"]) > .field-why:not([open]) {
  display: inline-block;
  vertical-align: middle;
}

.field-why > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  margin-left: 0.35rem;
  color: var(--muted);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
  list-style: none;
}

.field-why > summary::-webkit-details-marker { display: none; }

.field-why > summary:hover {
  color: var(--fg);
  border-color: var(--fg);
}

.field-why > summary:focus-visible {
  outline: 2px solid transparent;
  box-shadow: var(--focus-ring);
}

.field-why > summary svg { width: 0.72rem; height: 0.72rem; }

.field-why > p {
  width: 26rem;
  max-width: 100%;
  margin: 0.4rem 0 0.5rem;
  padding: 0.5rem 0.7rem;
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 400;
  /* Why we ask is information, so it is the blue note (#301). */
  background: var(--info-bg);
  border-radius: 0 6px 6px 0;
}

button {
  font: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  padding: 0.5rem 1.1rem;
  border-radius: var(--radius-btn);
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
}

button:hover { background: var(--accent-hover); }

/* Secondary actions: "Add another" on collection steps. */
button[name="action"][value="add"] {
  background: var(--card);
  color: var(--fg);
  border-color: var(--line-strong);
}

button[name="action"][value="add"]:hover { background: var(--raised); }

.btn,
.quick-links a {
  display: inline-block;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  text-decoration: none;
  padding: 0.5rem 1.1rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius-btn);
}

.btn:hover, .quick-links a:hover { background: var(--accent-hover); }

/* .btn sets no font-size, so an <a class="btn"> lands at the page's 1rem
   while a <button> beside it is 0.92rem. This matches the pair up where an
   anchor-button sits in a row of real buttons (#227). Scoped rather than
   put on .btn itself, which would resize every anchor-button in the app;
   the wider gap is its own issue. */
.btn-compact { font-size: 0.92rem; }

/* Join an online session (issue #179): a small primary button. */
.join-link { font-size: 0.82rem; padding: 0.25rem 0.7rem; margin-left: 0.4rem; }
.event-detail-join { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.75rem; }

.quick-links {
  list-style: none;
  padding: 0;
  display: flex;
  gap: 0.75rem;
  margin: 1.5rem 0;
}

.quick-links li + li a {
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line-strong);
}

.quick-links li + li a:hover { background: var(--raised); }

/* ---- Wizard ------------------------------------------------------------ */

.wizard-progress {
  color: var(--muted);
  margin-bottom: 0.25rem;
}

.wizard-blurb { color: var(--muted); margin-top: 0; }

.wizard-note { color: var(--muted); font-size: 0.85rem; }

/* The wizard's back link is a link, not a button — same ink-plus-underline
   treatment as `a`, restated explicitly because it carries no <a>. Disclosure
   summaries used to share this rule; they are Secondary buttons now (see
   details.log-form > summary below). */
.wizard-back {
  color: var(--fg);
  font-weight: 600;
  text-decoration: none;
  background: linear-gradient(var(--link-line), var(--link-line)) 0 100% / 100% 2px no-repeat;
  padding-bottom: 2px;
  cursor: pointer;
}

.wizard-back { margin-right: 1rem; }

/* ---- Dashboard sections ------------------------------------------------ */

.dashboard-section,
.wellbeing-widget {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1.1rem 1.4rem 1.25rem;
  margin: 1rem 0;
}

.dashboard-section h2,
.wellbeing-widget h2 {
  font-size: 1.05rem;
  margin: 0 0 0.75rem;
}

.enrolment-summary { color: var(--muted); margin: 1.25rem 0 0.5rem; }

.wellbeing-form {
  display: flex;
  gap: 0.5rem;
}

.wellbeing-form button {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-sq);
  color: var(--fg);
  font-size: 1.35rem;
  line-height: 1;
  padding: 0.3rem 0.45rem;
}

.wellbeing-form button:hover { background: var(--select-wash); }

.last-checkin { color: var(--muted); font-size: 0.85rem; margin: 0.6rem 0 0; }

/* Requirement cards — the progress row is the heart of the dashboard. */
.requirement-cards {
  list-style: none;
  padding: 0;
  margin: 0.75rem 0 1.25rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 0.75rem;
}

.requirement-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.85rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.requirement-label { font-weight: 600; font-size: 0.9rem; }

.requirement-target,
.requirement-progress,
.requirement-ratio {
  color: var(--muted);
  font-size: 0.85rem;
}

.progress {
  height: 6px;
  border-radius: 3px;
  background: var(--line);
  overflow: hidden;
  margin-top: 0.3rem;
}

.progress-fill {
  display: block;
  height: 100%;
  max-width: 100%;
  border-radius: 3px;
  /* --ok-fg, not --green: raw green against the --line track is 1.01:1
     on light, so the bar showed nothing. --ok-fg is surface-aware (ink on
     light, 11.78:1 against the track; green in dark). */
  background: var(--ok-fg);
}

/* Charge-or-waive dialog (issue #172): the skeleton's choice cards, stacked.
   When the skeleton lands its own .choice-card rule, keep one copy. */
/* Online booking settings: new and existing clients' approval, side by side (#321). */
.approval-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0.75rem 1.5rem; align-items: start; }
.approval-pair fieldset { margin: 0; min-width: 0; }

.choice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin: 0.75rem 0; padding: 0; border: 0; }
.choice-grid.choice-stack { grid-template-columns: 1fr; gap: 0.5rem; }
.choice-card { position: relative; display: block; background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 0.8rem 0.9rem 0.85rem 2.6rem; cursor: pointer; color: var(--fg); }
.choice-card:hover { border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); }
.choice-card input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.choice-card .fc-sq { position: absolute; left: 0.9rem; top: 1rem; color: var(--accent); }
.choice-card:has(input:checked) { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: var(--select-wash); }
.choice-card:has(input:focus-visible) { outline: 2px solid transparent; box-shadow: var(--focus-ring); }
.choice-card:has(input:checked:focus-visible) { box-shadow: inset 0 0 0 1px var(--accent), var(--focus-ring); }
.choice-card h3 { margin: 0 0 0.15rem; font-size: 0.98rem; }
.choice-card p { margin: 0; color: var(--muted); font-size: 0.88rem; }
.fee-when { color: var(--muted); margin: 0 0 0.75rem; font-size: 0.92rem; }
.fee-policy { font-size: 0.92rem; margin: 0 0 0.5rem; }
.fee-tells { color: var(--muted); font-size: 0.85rem; margin: 0.75rem 0 0; }
.dialog-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.75rem; }
#event-detail.is-wide { max-width: 30rem; width: calc(100vw - 2rem); }

.journal-flag { margin: 0.75rem 0 0; }
.journal-flag label { display: inline-flex; gap: 0.4rem; align-items: center; font-weight: 600; font-size: 0.9rem; }
.journal-flag .helptext { display: block; }
/* Now inside the editor shell, ahead of Cancel/Save in tab order (issue
   #177): match the shell's own horizontal padding so it does not sit
   flush against the card edge. */
.journal-editor .journal-flag { margin: 0.75rem 1.35rem; }

.requirement-milestone { display: block; font-weight: 600; color: var(--fg); }

/* ---- "For your attention", one card (#178's shape, #212's rows) ---------
   The qualified dashboard used to stack separate info stripes. Legible at
   two rows, a wall at six, and every row shouted equally. One card, plain
   rows inside: a bold lead, a muted rest, and the action pushed to the
   right so the eye can run down the links. The warning glyph is per row,
   not per card, because most of these are nudges and only some are
   running out. The card's own blue left bar retired (Task 3): a whole
   card is not an aside, and the per-row glyph already carries the signal
   where a row genuinely needs it. */

.widget-attention {
  margin-bottom: 1.25rem;
}
.attention-list {
  list-style: none;
  padding: 0;
  margin: 0.25rem 0 0;
}
.attention-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.6rem;
  padding: 0.45rem 0.1rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}
.attention-list li:last-child {
  border-bottom: none;
}
.attention-list .attention-lead {
  font-weight: 600;
}
.attention-list .attention-rest {
  color: var(--muted);
}
.attention-list li > a {
  margin-left: auto;
  white-space: nowrap;
  font-size: 0.85rem;
}
.attention-list .attention-warn {
  color: var(--warn-fg);
  font-weight: 700;
}

@media (max-width: 520px) {
  /* The action stops being a right-hand column and becomes the row's
     last line, so a long lead does not squeeze the link to one word. */
  .attention-list li > a {
    margin-left: 0;
    flex-basis: 100%;
  }
}

/* ---- Milestone alert banners --------------------------------------------- */

.milestone-alerts {
  list-style: none;
  padding: 0;
  margin: 0.75rem 0 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.milestone-alert {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  border-radius: var(--radius-card);
  padding: 0.7rem 1rem;
  font-size: 0.92rem;
}

/* Paint retired for .note-warn (Task 4): the markup for report_due and
   the literal "warning" prediction banners pairs
   class="milestone-alert milestone-alert-report_due note-warn" (or
   -warning), so .note-warn now supplies the wash, the --fg body copy and
   the stripe that this rule used to paint itself, matching the recipe
   .gate-notice already uses. .milestone-alert keeps its own
   0.7rem 1rem padding and var(--radius-card) radius above (it is shared
   layout across pre_alert too, which is not swapped — see below), so
   those two properties on .note-warn are redundant here rather than
   conflicting: same cascade position, .milestone-alert simply wins on
   source order and the two agree on radius already.

   .milestone-alert-report_due and .milestone-alert-warning stay in the
   template's class list even with no CSS of their own: the demo runner
   (tests/demo/steps.py) locates the prediction banner by
   `li.milestone-alert-warning`, and apps/enrolments/tests/test_dashboard_
   milestones.py asserts "milestone-alert-report_due" is absent from the
   dashboard body when no report is due. */

.milestone-alert-pre_alert {
  /* Two-step wash, as .messages li — a border at the same strength as its
     own fill is invisible. --ok-fg, not raw --green (Task 3): a 40% mix
     of the raw token is still neon on a light surface. */
  background: color-mix(in srgb, var(--green) 14%, transparent);
  color: var(--fg);
  border: 1px solid color-mix(in srgb, var(--ok-fg) 40%, transparent);
}

/* .milestone-alert-info retired (Task 3): no banner.kind value ever
   produced "info" (only report_due, pre_alert and the literal "warning"
   predictions use), so the rule painted nothing. Blue informs without an
   alarm now lives in the .note-info component instead of a per-widget
   stripe. */

.milestone-alert-name { font-weight: 600; }

.milestone-alert .btn { margin-left: auto; }

/* ---- Milestone-reports section chips ------------------------------------- */

.milestone-report-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.milestone-report-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.45rem 0.1rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}

.milestone-report-list li:last-child { border-bottom: none; }

.milestone-report-name { font-weight: 600; }

.milestone-report-list li > a { margin-left: auto; font-size: 0.85rem; }

.milestone-report-note { color: var(--muted); font-size: 0.85rem; margin-left: auto; }

/* ---- Record rows ------------------------------------------------------- */

.placement-list, .supervisor-list, .counsellor-list,
.session-list, .supervision-list, .personal-session-list,
.wbl-list, .other-list, .client-list, .journal-list,
.document-list,
.membership-list,
.cpd-list,
.wizard-records {
  list-style: none;
  padding: 0;
  margin: 0 0 0.75rem;
}

.placement-list li, .supervisor-list li, .counsellor-list li,
.session-list li, .supervision-list li, .personal-session-list li,
.wbl-list li, .other-list li, .client-list li,
.document-list li,
.membership-list li,
.wizard-records li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
  padding: 0.45rem 0.1rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}

.placement-list li:last-child, .supervisor-list li:last-child,
.counsellor-list li:last-child, .session-list li:last-child,
.supervision-list li:last-child, .personal-session-list li:last-child,
.wbl-list li:last-child, .other-list li:last-child,
.client-list li:last-child, .journal-list li:last-child,
.document-list li:last-child,
.membership-list li:last-child,
.cpd-list li:last-child,
.wizard-records li:last-child {
  border-bottom: none;
}

.placement-list li > span:first-child,
.session-list li > span:first-child,
.supervision-list li > span:first-child,
.personal-session-list li > span:first-child,
.wbl-list li > span:first-child,
.other-list li > span:first-child,
.client-list li > span:first-child,
.document-list li > span:first-child,
.membership-list li > span:first-child,
.supervisor-list li > span:first-child {
  font-weight: 600;
}

/* CPD rows (#326), laid out like .payment-list: one entry per row on a
   grid, the title with its type and provider beneath, hours right-aligned
   in tabular figures. Below 600px the same cells restack into three
   lines (title and hours; date and status; actions) rather than
   wrapping mid-row. */
.cpd-list li {
  display: grid;
  /* Every column fixed but the title's: each row is its own grid, so an
     auto column would size per row and knock the hours out of line. */
  grid-template-columns: 6.5rem minmax(0, 1fr) 4rem 5.5rem 9rem;
  grid-template-areas: "date main hours status links";
  gap: 0.2rem 0.9rem;
  align-items: baseline;
  padding: 0.6rem 0.1rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}
.cpd-list li:last-child { border-bottom: none; }
.cpd-list .cpd-date { grid-area: date; color: var(--muted); }
.cpd-list .cpd-main { grid-area: main; display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.cpd-list .cpd-title { font-weight: 600; overflow-wrap: anywhere; }
.cpd-list .cpd-meta { color: var(--muted); font-size: 0.85rem; }
.cpd-list .cpd-hours { grid-area: hours; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.cpd-list .cpd-status { grid-area: status; }
.cpd-list .cpd-links { grid-area: links; display: inline-flex; gap: 0.9rem; justify-content: flex-end; font-size: 0.85rem; }
@media (max-width: 600px) {
  .cpd-list li {
    /* The actions span the last two columns, so "Certificate  Delete"
       never widens the hours column and squeezes the title. */
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    grid-template-areas:
      "main main main hours"
      "date status links links";
    gap: 0.35rem 0.75rem;
  }
}

/* .document-list and .membership-list are deliberately NOT in this group,
   though they share every other rule above. Both carry a validity pill
   (UI refresh phase 2) among their spans, and this selector's specificity
   beats .pill's `color: var(--pill-fg, var(--fg))` — adding them here
   flattens Current, Expiring soon and Expired to the same grey. Measured
   while mocking #164, still true now the chip is a pill. */
.placement-list li > span:not(:first-child),
.session-list li > span:not(:first-child),
.supervision-list li > span:not(:first-child),
.personal-session-list li > span:not(:first-child),
.wbl-list li > span:not(:first-child),
.other-list li > span:not(:first-child),
.supervisor-list li > span:not(:first-child),
.client-list li > span:not(:first-child) {
  color: var(--muted);
}

.session-list li > a, .supervision-list li > a,
.personal-session-list li > a, .wbl-list li > a,
.other-list li > a, .client-list li > a,
.placement-list li > a {
  margin-left: auto;
  font-size: 0.85rem;
}

/* A supervisor row carries phone and email links too (#314), so only its
   Edit link is pushed to the end. */
.supervisor-list li > a.supervisor-edit {
  margin-left: auto;
  font-size: 0.85rem;
}

/* A defect this codebase has hit more than once: this chip painted
   --danger-fg text on its own --danger-bg wash, which fails
   the dark theme (4.40:1, under the 4.5:1 a 0.75rem/600 chip needs).
   Copy moves to --fg, as the rule requires, and the danger signal stays
   visible as a border in --danger-accent — the literal brand red used at
   full strength (not the 40%-mix step .messages li.error uses for its
   outer border) so it still reads at this chip's much smaller size.
   --danger-accent is theme-invariant and clears 3:1 on every surface in
   both themes, the same reason .messages li.error's stripe uses it rather
   than the theme-variant --danger-fg. */
.session-outcome-flag,
.client-ended .client-status {
  background: var(--danger-bg);
  color: var(--fg);
  border: 1px solid var(--danger-accent);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.05rem 0.5rem;
  border-radius: var(--radius-sq);
}

.client-ended > span { opacity: 0.7; }

.journal-list li { padding: 0.6rem 0.1rem; border-bottom: 1px solid var(--line); }
.journal-title { font-weight: 600; }
.journal-when { color: var(--muted); font-size: 0.85rem; margin-left: 0.6rem; }
.journal-body { margin: 0.25rem 0 0; }
.journal-privacy { color: var(--muted); font-size: 0.85rem; margin-top: -0.5rem; }

/* Add/log forms fold away so the dashboard reads as a dashboard. */
details.log-form { margin-top: 0.35rem; }

details.log-form[open] summary { margin-bottom: 0.85rem; }

/* Wellbeing check-ins past the newest ten (issue #415): a native
   disclosure, so it works without JavaScript and opens with Enter/Space. */
details.checkin-more { margin-top: 0.5rem; }
details.checkin-more > summary { cursor: pointer; font-weight: 600; padding: 0.35rem 0; }

/* Supervision session page (issue #177). Facts as a two-column list, the
   agenda as record rows with a kind chip, the confirm as the app's
   dialog recipe (.event-detail, dialog.journal-confirm). */
.record-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 1rem;
  margin: 0 0 0.75rem;
  max-width: 40rem;
  font-size: 0.92rem;
}
.record-facts dt { color: var(--muted); }
.record-facts dd { margin: 0; }
.record-actions { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; align-items: center; }

/* The row's one destructive action sits apart from routine actions, at the
   end of the row (UI refresh phase 3, ruling Q4), the same margin-left:
   auto push .page-actions > .btn-secondary:last-child already uses. */
.record-actions > .record-action-danger { margin-left: auto; }

.agenda-intro { color: var(--muted); margin: 0 0 0.5rem; font-size: 0.92rem; }
.agenda-list { list-style: none; padding: 0; margin: 0 0 0.75rem; }
.agenda-list li {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.9rem;
  padding: 0.45rem 0.1rem; border-bottom: 1px solid var(--line); font-size: 0.92rem;
}
.agenda-list li:last-child { border-bottom: none; }
.agenda-who { font-weight: 600; }
.agenda-when, .agenda-theme { color: var(--muted); }
.agenda-list li > a { margin-left: auto; font-size: 0.85rem; }
.agenda-done { color: var(--muted); font-size: 0.85rem; margin: 0.35rem 0 0; }
.sup-next { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.3rem 1rem; margin: 0; }
.supervision-list .supervision-date a { font-weight: 600; }

/* The shared dialog recipe: the replace-content guard (journal) and the
   didn't-happen confirm (supervision, other records) are the same card,
   just a different width and a different action row. */
dialog.journal-confirm, dialog.record-confirm {
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  padding: 1.25rem 1.5rem;
}
dialog.journal-confirm::backdrop, dialog.record-confirm::backdrop { background: rgba(29, 34, 56, 0.55); }
dialog.journal-confirm h2, dialog.record-confirm h2 { font-size: 1.1rem; margin: 0 0 0.3rem; }
dialog.journal-confirm p, dialog.record-confirm p { margin: 0 0 1rem; color: var(--muted); font-size: 0.95rem; }
dialog.journal-confirm { max-width: 26rem; }
dialog.journal-confirm form { display: flex; gap: 0.5rem; justify-content: flex-end; }
dialog.record-confirm { max-width: 28rem; }
.record-confirm-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: flex-end; }
.record-confirm-actions form { margin: 0; }

/* ---- Placement checklist ------------------------------------------------ */

.checklist {
  list-style: none;
  padding: 0;
  margin: 1rem 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.checklist-item {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.85rem 1rem;
}

.checklist-item-body { flex: 1; min-width: 0; }

.checklist-item-text { font-weight: 600; display: block; }

.checklist-item-guidance {
  color: var(--muted);
  font-size: 0.85rem;
  display: block;
  margin: 0.15rem 0 0.25rem;
}

.checklist-item-form {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.checklist-item-form select { width: auto; }

.checklist-placement { color: var(--muted); margin-top: 0; }

@media (max-width: 600px) {
  body > header { padding: 0.85rem 1rem; }
  body > header nav .user-email { display: none; }
}

/* ---- App shell (templates/app_base.html) --------------------------------
   Every rule below is scoped under .app-shell so public pages (which use
   base.html, never app_base.html) are unaffected. Where an app-shell class
   sits on an element base.css already styles by tag selector (body >
   header, nav a, main), properties are redeclared in full here rather than
   relying on the inherited cascade — a class selector always outranks a
   type selector, but self-contained rules are safer against future edits
   to the tag-level rules above. */

.app-shell {
  display: grid;
  grid-template-columns: 230px 1fr;
  min-height: 100vh;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Pinned to the WINDOW, not to the content. .app-shell is a
   min-height: 100vh grid, so a stretched grid item is as tall as the
   page; align-self: start plus an explicit height opts out of that, and
   sticky keeps it in view as the content scrolls past. The sidebar
   itself never scrolls: the nav list below owns that, so the Settings
   and Log out footer is always on screen (issue #83). */
.app-shell-sidebar {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  padding: 1.25rem 1rem;
  border-right-width: 1px;
  border-right-style: solid;
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow: hidden;
}

/* min-height: 0 is load-bearing: a flex item defaults to min-height: auto
   and refuses to shrink below its content, which would push the footer
   out of the sidebar rather than scroll the list. */
.app-shell-nav {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* The lockup — a strapline-free transparent SVG, so no mask is needed.
   See templates/_logo.html and BRAND.md. */
.fc-logo {
  display: inline-block;
  vertical-align: middle;
  line-height: 0;
}

.fc-logo img { height: 34px; width: auto; }

/* The shell is the ink surface in BOTH themes; only the content area
   follows data-theme. */
.app-shell-sidebar,
.app-shell-topbar {
  background: var(--shell-bg);
  color: var(--shell-fg);
  border-color: var(--shell-line);
}

.app-shell-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.app-shell-nav-link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-btn);
  color: var(--shell-fg);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.92rem;
}

.app-shell-nav-link svg {
  width: 1.15rem;
  height: 1.15rem;
  flex: none;
}

.app-shell-nav-link:hover {
  background: color-mix(in srgb, var(--shell-accent) 14%, transparent);
  color: #ffffff;
}

/* Active row: FC green, which is legible on ink in both themes — the
   whole reason the shell stays ink. */
.app-shell-nav-link.is-active {
  background: color-mix(in srgb, var(--shell-accent) 18%, transparent);
  color: var(--shell-accent);
}

/* The pilot's Feedback button (#246): the shell's one primary action, so
   it takes the filled treatment rather than a nav row's tint. Green is a
   fill behind an ink label (BRAND.md), legible because the shell is ink in
   both themes. Its twin in the mobile topbar is .app-shell-topbar-cta. */
.app-shell-nav-link.app-shell-nav-cta {
  background: var(--shell-accent);
  color: var(--ink);
}

.app-shell-nav-link.app-shell-nav-cta:hover {
  background: var(--shell-accent-hover);
  color: var(--ink);
}

.app-shell-topbar-cta {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.75rem;
  border-radius: var(--radius-btn);
  background: var(--shell-accent);
  color: var(--ink);
  font-weight: 600;
  font-size: 0.85rem;
  text-decoration: none;
}

.app-shell-topbar-cta:hover {
  background: var(--shell-accent-hover);
  color: var(--ink);
}

.app-shell-topbar-cta svg {
  width: 1rem;
  height: 1rem;
  flex: none;
}

/* Settings and Log out stay pinned to the bottom of the sidebar, which is
   itself the height of the window. They share one block under one divider,
   spaced like the nav list above (issue #55). flex: none stops the footer
   being the thing that shrinks when the nav list is too tall (issue #83);
   margin-top: auto still does the work when the list is shorter. */
.app-shell-nav-settings {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: none;
  margin-top: auto;
  padding-top: 1rem;
  border-top: 1px solid;
  border-top-color: var(--shell-line);
}

/* Log out is a <button> because allauth signs a user out on POST only
   (see apps/core/context_processors.py), but it is a nav row, not an
   action: the green-is-a-fill rule reserves the filled treatment for
   primary actions (BRAND.md). Undo just the two properties the global
   `button` rule contributes that .app-shell-nav-link doesn't already
   override, and let the row stretch so its hover fills the sidebar's
   width like every link beside it. The focus ring comes from the
   sidebar's own green override; do not add a local one. */
button.app-shell-nav-link {
  width: 100%;
  background: none;
  border: 0;
}

/* The shell is ink in both themes, so its focus ring is always green —
   11.7:1 on ink, and the brand's own answer wherever it is legible. */
.app-shell-sidebar,
.app-shell-topbar {
  --ring-gap: var(--shell-bg);
  --ring-inner: var(--green);
  --ring-outer: transparent;
}

.app-shell-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.app-shell-topbar {
  display: none; /* shown only below the drawer breakpoint, further down */
}

.app-shell-main {
  flex: 1 0 auto;
  width: 100%;
  max-width: 1100px;
  margin: 2rem auto 4rem;
  padding: 0 1.5rem;
}

.app-shell-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border-radius: var(--radius-btn);
  border: 1px solid var(--shell-line);
  background: transparent;
  color: var(--shell-fg);
  cursor: pointer;
}

.app-shell-toggle svg {
  width: 1.2rem;
  height: 1.2rem;
}

.app-shell-toggle-icon-close { display: none; }

.app-shell.is-drawer-open .app-shell-toggle-icon-open { display: none; }
.app-shell.is-drawer-open .app-shell-toggle-icon-close { display: inline-flex; }

.app-shell-scrim {
  display: none;
}

/* Drawer breakpoint: the sidebar goes off-canvas and a topbar toggle takes
   over. static/js/shell.js toggles .app-shell.is-drawer-open and syncs
   aria-expanded on #app-shell-toggle; see also templates/app_base.html. */
@media (max-width: 900px) {
  .app-shell {
    display: block;
  }

  .app-shell-topbar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.85rem 1rem;
    border-bottom-width: 1px;
    border-bottom-style: solid;
    /* Above the drawer's z-index (40) so the toggle stays reachable while
       the sidebar is open — otherwise the fixed, full-height sidebar
       paints over it and a real pointer can't reach the close control. */
    position: relative;
    z-index: 50;
  }

  .app-shell-sidebar {
    position: fixed;
    inset: 0 25% 0 0;
    /* inset already sizes it to the viewport; this overrides the 100vh
       the desktop rule sets, and overflow: hidden hands the scrolling to
       the nav list so the footer stays put here too (issue #83). */
    height: auto;
    z-index: 40;
    overflow: hidden;
    transform: translateX(-100%);
    /* Closed: leave the a11y tree (visibility: hidden) only once the slide-
       out finishes, so links aren't keyboard-tabbable while off-canvas. */
    visibility: hidden;
    transition: transform 0.2s ease, visibility 0s 0.2s;
    box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
  }

  .app-shell.is-drawer-open .app-shell-sidebar {
    transform: translateX(0);
    /* Open: rejoin the a11y tree immediately, no delay. */
    visibility: visible;
    transition: transform 0.2s ease, visibility 0s;
  }

  .app-shell-scrim:not([hidden]) {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: rgba(20, 18, 15, 0.35);
  }

  .app-shell-main {
    padding: 1.5rem 1rem 3rem;
  }
}

/* ──────────────────────────────────────────────────────────────────────
   Calendar (apps/scheduling)

   One colour token per event type; the feed sends var() references, so
   the palette lives here and not in Python (apps/scheduling/services.py
   builds var(--cal-<type>) from the event type — do not rename these).

   Each type is anchored on a brand hue, held at that hue's OKLCH angle
   and stepped in lightness until it clears the chart checks. The pure
   brand hexes cannot be used directly: five of them fall outside the
   categorical lightness band (FC green sits at L 0.88, yellow at 0.82),
   which is a *chart* constraint and does not apply to the module-key
   squares elsewhere in this file.

   Client session takes blue, which reinforces "blue = bookings" rather
   than competing with it; the module key is not in play inside a
   calendar view. Other activity is the deliberate achromatic "Other"
   slot and sits outside the categorical validation.

   Validated with the dataviz six-checks (validate_palette.js, 2026-08-11)
   under --pairs all, the correct pairlist here because any two event
   types can sit side by side in a day or week view:
     light, surface #eff1f5 — worst all-pairs CVD ΔE 10.6 (deutan),
       normal-vision 18.3, all >= 3:1, all five checks PASS
     dark,  surface #252b45 — worst all-pairs CVD ΔE  8.4 (deutan),
       normal-vision 17.2, all >= 3:1, all five checks PASS
   The dark green/yellow pair sits at tritan ΔE 3.8, which the standard
   does not gate; every event also carries a visible text label, so
   identity is never colour-alone.

   Each event is drawn in its state's look (ruling C5, see "Event looks"
   below): a tint for confirmed, dashed for unconfirmed or awaiting card,
   muted for did not attend. The look is a second encoding, independent of
   hue; the future/past fill-vs-outline split it replaces is gone. */
:root {
  --cal-client-session: #005790;       /* blue   — bookings */
  --cal-supervision: #dd00d4;          /* purple */
  --cal-personal-counselling: #009c65; /* green  */
  --cal-placement-activity: #8d5e00;   /* yellow */
  --cal-other-activity: #5e616d;       /* the "Other" slot, deliberately grey */
  --cal-cpd-activity: #960039;         /* red    — the retired "personal" slot's validated hex (#326) */
}

:root[data-theme="dark"] {
  --cal-client-session: #0081d0;
  --cal-supervision: #ee00e4;
  --cal-personal-counselling: #00935f;
  --cal-placement-activity: #a56f00;
  --cal-other-activity: #a7acbe;
  --cal-cpd-activity: #ff3570;
}

#calendar {
  margin-top: 1rem;
  /* Anchors .cal-gap-gutter (issue #97), an absolutely positioned overlay. */
  position: relative;
  /* FullCalendar's own theming hooks, mapped to the brand tokens. */
  --fc-border-color: var(--line);
  --fc-page-bg-color: var(--card);
  --fc-neutral-bg-color: var(--bg);
  /* Toolbar buttons are Secondary controls now (brief section 3): a card
     surface with a line border, so the one thing on the bar that still
     reads as selected is the active view segment (month/week/day, and
     the toolbar "+"), filled in ink. */
  --fc-button-bg-color: var(--card);
  --fc-button-border-color: var(--line-strong);
  --fc-button-text-color: var(--fg);
  --fc-button-hover-bg-color: var(--raised);
  --fc-button-hover-border-color: var(--line-strong);
  --fc-button-active-bg-color: var(--accent);
  --fc-button-active-border-color: var(--accent);
  /* Today's wash stays raw green — a highlight, not a control, so it
     never needed the 3:1 boundary the buttons above do. Stepped down
     from 16% to 6% now the toolbar itself carries an ink fill, so
     today's column no longer competes with the active view segment. */
  --fc-today-bg-color: color-mix(in srgb, var(--green) 6%, transparent);
  /* Events are tints of their type colour now (ruling C5), so their
     text is body ink, not white. */
  --fc-event-text-color: var(--fg);
  /* Leftover vendored defaults (phase 4 task 5): none of these currently
     paint in our views — selectable, list view and a "+more" link are
     none of them wired up here — but each is still mapped, so a leftover
     default can never surface the vendored bundle's own light-theme
     colours (a blue event, a grey highlight, a plain "red" now-line) if
     one of those features is ever turned on, especially in the dark
     theme. */
  --fc-highlight-color: var(--select-wash);
  --fc-list-event-hover-bg-color: var(--raised);
  --fc-event-bg-color: var(--card);
  --fc-event-border-color: var(--line);
  /* Brief §4.2 keeps the now-indicator line — the brand's red, not the
     vendored bundle's plain "red" keyword, so dark gets the stepped
     value. */
  --fc-now-indicator-color: var(--danger-fg);
  /* Task 5 review: every event is keyboard-focusable (tabindex 0), and the
     vendored default here is a black 25% overlay on .fc-event:focus:after
     - worst on dark, and never our own ring. Suppressed outright below
     (#calendar .fc-event:focus::after { display: none }), so this is
     belt-and-braces: even if that rule were ever dropped, transparent
     paints nothing. --fc-bg-event-color/--fc-bg-event-opacity are the one
     deliberately unmapped pair left: this app has no background events. */
  --fc-event-selected-overlay-color: transparent;
}

/* The quiet line above the toolbar: "Showing next week" (#315) on the
   left, only there when the week view has moved past a finished working
   week, and "Updated just now · Refresh" (#317) pushed to the right. */
.cal-status-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 1rem;
  margin: 0 0 0.5rem;
}

.cal-week-ahead,
.cal-updated {
  margin: 0;
  font-size: 0.875rem;
  color: var(--muted);
}

.cal-updated { margin-left: auto; }

/* A link-weight action, not a second filled button competing with the
   toolbar's own. */
.cal-updated button,
.cal-updated button:hover {
  font: inherit;
  padding: 0;
  margin-left: 0.35rem;
  border: 0;
  background: none;
  color: var(--fg);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.cal-updated button:disabled { color: var(--muted); cursor: progress; }

/* The one non-working-time hatch (issue #97 gutter, #267 business hours):
   both take it, at the same opacity, so they read as one. The legend
   under the calendar shows it too, so it is declared on both. */
#calendar,
.cal-legend {
  --cal-hatch: repeating-linear-gradient(
    45deg,
    transparent 0 4px,
    var(--line) 4px 5px
  );
}

/* FullCalendar paints the active button's own label from
   --fc-button-text-color above (now --fg, the Secondary resting state),
   so the active/pressed view segment needs its label restated here in
   --on-accent to stay legible against its ink fill. */
#calendar .fc-button-primary:not(:disabled).fc-button-active,
#calendar .fc-button-primary:not(:disabled):active { color: var(--on-accent); }

/* The vendored bundle (index.global.min.js) injects its own focus box
   shadows on a bare :focus, one on .fc-button at (0,3,0) and a
   higher-specificity one on the pressed month/week/day segment's
   .fc-button-primary at (0,5,0) — see _plain_focus_violations in
   test_brand.py for why that shape is a defect in OUR css, and note the
   vendored bundle is explicitly out of scope for it. Neither brand ring
   ever showed through them. #calendar (an id) outranks any number of
   classes/pseudo-classes at (1,2,0), so it wins outright regardless of
   how many classes those chain. :focus (fires for mouse clicks too)
   clears the box shadow; :focus-visible (keyboard/programmatic only)
   restates the brand ring after it in source order, so a mouse click
   shows nothing and a Tab shows the double ring. */
#calendar .fc-button:focus {
  box-shadow: none;
}
#calendar .fc-button:focus-visible {
  outline: 2px solid transparent;
  box-shadow: var(--focus-ring);
}

/* Events are clickable (they open the detail dialog); say so (issue #97). */
#calendar .fc-event {
  cursor: pointer;
}

/* B10: the event a dialog save just created scales in, once (calendar.js
   adds the class on its first mount only, and never under reduced
   motion; the media query below is the backstop). */
#calendar .fc-event.cal-just-added {
  animation: cal-just-added 0.35s ease-out;
}
@keyframes cal-just-added {
  from { opacity: 0; transform: scale(0.92); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  #calendar .fc-event.cal-just-added { animation: none; }
}

/* Toolbar v2 (issue #97): the two zoom toggles are pill switches,
   visually separated from the month/week/day segmented control by a
   divider drawn on .fc-fullWeekToggle-button::after, not the view
   group. As a pseudo-element of that button it also inherits the
   pill's disabled dimming below - accepted. Ink is the control accent on
   light surfaces now (brief section 1.2); the toolbar buttons above went
   Secondary in this task, so the pressed state fills solid, ink with a
   green label, so it still reads as selected beside them. */
#calendar .fc-fullDayToggle-button,
#calendar .fc-fullWeekToggle-button {
  border-radius: 999px;
  background: transparent;
  border-color: var(--line);
  color: var(--muted);
}
#calendar .fc-fullDayToggle-button[aria-pressed="true"],
#calendar .fc-fullWeekToggle-button[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
#calendar .fc-fullDayToggle-button:disabled,
#calendar .fc-fullWeekToggle-button:disabled {
  opacity: 0.4;
}
#calendar .fc-fullWeekToggle-button {
  margin-right: 1.25rem;
  position: relative;
}
#calendar .fc-fullWeekToggle-button::after {
  content: "";
  position: absolute;
  right: -0.75rem;
  top: 15%;
  height: 70%;
  border-left: 1px solid var(--line);
}
#calendar .fc-newEntry-button {
  font-weight: 700;
  min-width: 2.2em;
}

/* Toolbar on a narrow calendar (issues #497, #506): FullCalendar lays the
   toolbar out as three chunks in one row, which squeezed the title to two
   to four lines and, on a phone, pushed the page wider than the screen.
   When the calendar is too narrow for that row, the chunks dissolve
   (display: contents) so the title takes its own row on top and the
   buttons wrap underneath in DOM order, keeping tab order unchanged.
   The divider between the pills and the view group would dangle at a row
   end once they wrap, so it goes.

   This asks how wide the calendar is, not the screen: above the 900px
   drawer breakpoint the sidebar comes back, so a 1024px tablet leaves the
   calendar narrower than an 820px one does. The row needs 609px of
   buttons plus the title; the longest day or month title ("30 September
   2026") makes that about 882px. Week view abbreviates its months
   (#509, calendar.js) so its title fits the same row. Phones are well
   inside this; calendar.js's own 640px phone default view is a separate
   choice and unchanged. */
#calendar { container-type: inline-size; }
@container (max-width: 889px) {
  #calendar .fc-header-toolbar {
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
  }
  #calendar .fc-toolbar-chunk { display: contents; }
  #calendar .fc-toolbar-title {
    order: -1;
    flex-basis: 100%;
  }
  #calendar .fc-header-toolbar .fc-button {
    min-height: 2.5rem;
    padding-left: 0.6em;
    padding-right: 0.6em;
  }
  /* FullCalendar spaces each chunk's children with a left margin; the
     gap above does that job now. */
  #calendar .fc-header-toolbar > .fc-toolbar-chunk > * { margin: 0; }
  #calendar .fc-fullWeekToggle-button { margin-right: 0; }
  #calendar .fc-fullWeekToggle-button::after { display: none; }
}

/* The same layout for browsers without container queries (iOS 15 Safari
   and older; issue #509), by screen width instead: the calendar is under
   890px wide at every screen up to 1167px, with or without the sidebar.
   Keep the two blocks in step. */
@supports not (container-type: inline-size) {
  @media (max-width: 1167px) {
    #calendar .fc-header-toolbar {
      flex-wrap: wrap;
      justify-content: flex-start;
      gap: 0.5rem;
      margin-bottom: 0.75rem;
    }
    #calendar .fc-toolbar-chunk { display: contents; }
    #calendar .fc-toolbar-title {
      order: -1;
      flex-basis: 100%;
    }
    #calendar .fc-header-toolbar .fc-button {
      min-height: 2.5rem;
      padding-left: 0.6em;
      padding-right: 0.6em;
    }
    #calendar .fc-header-toolbar > .fc-toolbar-chunk > * { margin: 0; }
    #calendar .fc-fullWeekToggle-button { margin-right: 0; }
    #calendar .fc-fullWeekToggle-button::after { display: none; }
  }
}

/* The one title longer than "30 September 2026": a week across New Year,
   "28 Dec 2026-3 Jan 2027" (calendar.js tags it cal-title-two-years). On a
   one-row toolbar just over 889px wide it needs about 16px more than it
   gets, so it wrapped at screens of about 1170px to 1230px (app#526 item
   15). It drops a size there instead; every other title, and the wrapped
   layout below 890px, is unchanged. The fallback covers the same screens
   for browsers without container queries; keep the two in step. */
@container (min-width: 890px) and (max-width: 1023px) {
  #calendar.cal-title-two-years .fc-toolbar-title { font-size: 1.375rem; }
}
@supports not (container-type: inline-size) {
  @media (min-width: 1168px) and (max-width: 1300px) {
    #calendar.cal-title-two-years .fc-toolbar-title { font-size: 1.375rem; }
  }
}

@media (max-width: 640px) {
  #calendar .fc-toolbar-title { font-size: 1.125rem; }

  /* The heading area (Stevie, on #503): "Updated ... Refresh" moves up
     beside the "Calendar" title instead of taking a row of its own. The
     calendar page's <main> becomes a two-column grid; the page header and
     the status row dissolve (display: contents) so the h1 and the note
     can share the first row, and everything else spans both columns in
     DOM order. The title keeps its natural width and a long note
     ("Updated 12 minutes ago") wraps within the rest. The subtitle stays,
     with tighter spacing. This stays a phone-only change (#506): on a
     tablet the note fits on its own row and the calendar still starts
     on the first screen. */
  .app-shell-main:has(> #calendar) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 0.75rem;
  }
  .app-shell-main:has(> #calendar) > *,
  .app-shell-main:has(> #calendar) > .fc-page-header > *,
  .app-shell-main:has(> #calendar) > .cal-status-row > * {
    grid-column: 1 / -1;
  }
  .app-shell-main:has(> #calendar) > .fc-page-header,
  .app-shell-main:has(> #calendar) > .cal-status-row {
    display: contents;
  }
  .app-shell-main:has(> #calendar) > .fc-page-header > h1 {
    grid-column: 1;
    grid-row: 1;
    align-self: center;
    margin: 0;
  }
  .app-shell-main:has(> #calendar) > .cal-status-row > .cal-updated {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    text-align: right;
  }
  .app-shell-main:has(> #calendar) > .fc-page-header > .fc-lede {
    margin: 0.25rem 0 0.75rem;
  }
  .app-shell-main:has(> #calendar) > .cal-status-row > .cal-week-ahead {
    margin-bottom: 0.5rem;
  }
  .cal-updated button,
  .cal-updated button:hover {
    min-height: 2.5rem;
  }
}

/* A hidden working day sat between these two columns (issue #97). */
#calendar .cal-gap-gutter {
  position: absolute;
  width: 18px;
  pointer-events: none;
  z-index: 2;
  background: var(--cal-hatch);
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
  opacity: 0.6;
}

/* Non-working time (issue #267, ruling C4): FullCalendar marks it from
   businessHours on its background layer, beneath the events, so an
   out-of-hours entry keeps its own look on top of the hatch.

   Regression found in phase 4 review: despite sitting "beneath the
   events", the hatch div is still a live hit target at an empty point in
   the grid, so it swallowed the pointer before it ever reached the plain
   slot lane underneath - paintGhost's own hit-test (static/js/calendar.js)
   never saw a hover there at all, and evening sessions past a short
   working day are common (brief §4.1), not an edge case. FullCalendar's
   own dateClick and drag-and-drop are unaffected (both resolve the
   pointer's cell geometrically, not by DOM target), so only the ghost
   needed this - but the hatch is purely decorative regardless, so
   pointer-events: none is correct for all of it, the same way
   .cal-gap-gutter (just above) is already none.

   .fc-non-business itself is not enough: FullCalendar wraps every
   background segment (business hours, a bg event, the selection
   highlight) in its own absolutely-positioned .fc-timegrid-bg-harness,
   which fully covers the same area and is *not* excluded by its child's
   pointer-events - skip the harness too, or the browser's hit-test just
   falls through to that instead. */
#calendar .fc-non-business {
  background: var(--cal-hatch);
  opacity: 0.6;
  pointer-events: none;
}
#calendar .fc-timegrid-bg-harness {
  pointer-events: none;
}

/* 30-minute grid (brief §4.1, phase 4 task 1, ruling C1): hour lines
   solid (the vendored bundle's own default), half-hours dotted. */
#calendar .fc-timegrid-slot-minor { border-top-style: dotted; }

/* Today's header (phase 4 task 4): an ink pill around the day-of-month
   number, and the word "Today" beside it - timeGrid views only
   (static/js/calendar.js's dayHeaderContent never runs for dayGridMonth).
   Another pill: test_brand.py's PILL_RADIUS_SELECTORS names it. */
#calendar .cal-today-num {
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
}
#calendar .cal-today-label {
  color: var(--ok-fg);
  font-size: 0.72rem;
  font-weight: 700;
  margin-left: 0.35rem;
}

/* Hover-to-create preview (issue #97): a translucent accent block the
   default slot length tall; never a pointer target. */
#calendar .cal-slot-ghost {
  position: absolute;
  pointer-events: none;
  z-index: 3;
  border-radius: var(--radius-btn);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  font-size: 0.8rem;
  padding: 2px 6px;
  overflow: hidden;
}

/* Drag-to-move mirror (issue #97): the floating copy FullCalendar renders
   under the cursor while a timed event is dragged (confirmed live: it is
   the same element carrying both .fc-event-mirror and .fc-event-dragging,
   never two separate nodes). Its kind colour arrives as inline
   border-color/background-color (apps/scheduling's per-kind --cal-*
   token), which a plain selector cannot outweigh - hence !important -
   swapped for the same translucent accent language as .cal-slot-ghost
   above, so "not yet committed" reads the same on offer (hover) and on
   move (drag). Also cancels the vendored stylesheet's own
   .fc-event-dragging opacity: .75, which would otherwise fade this
   twice over. */
#calendar .fc-event-mirror {
  background-color: color-mix(in srgb, var(--accent) 18%, transparent) !important;
  border-color: var(--accent) !important;
  color: var(--accent) !important;
  opacity: 1 !important;
}

/* Event looks (brief §4.3, rulings C5 and R4): one look per state, no
   corner glyphs. --c is the event's type colour, set per kind below from
   the --cal-* tokens. The feed sends no inline colours, so these id-scoped
   rules win over FullCalendar's own vendored ones without !important.

   Confirmed (a booked session still to come, an attended one, every
   non-clinical entry): a 14% tint of the type colour with a 3px solid
   left edge and --fg text. Unconfirmed (started, no outcome) and awaiting
   card (a booking hold): a --card fill inside a 1.5px dashed border in
   the type colour. Did not attend / late cancellation: the confirmed look,
   muted, title struck through. Contrast contracts for every type colour
   in both themes are in apps/core/tests/test_brand.py.

   The swatches in .cal-legend (below the calendar) reuse the same rules,
   which is why the look rules are not #calendar-scoped: a class plus
   .fc-event or .cal-legend-swatch. */
.cal-kind-client_session { --c: var(--cal-client-session); }
.cal-kind-supervision { --c: var(--cal-supervision); }
.cal-kind-personal_counselling { --c: var(--cal-personal-counselling); }
.cal-kind-placement_activity { --c: var(--cal-placement-activity); }
.cal-kind-other_activity { --c: var(--cal-other-activity); }
.cal-kind-cpd_activity { --c: var(--cal-cpd-activity); }

#calendar .fc-event,
.cal-legend-swatch {
  background: color-mix(in srgb, var(--c, var(--cal-other-activity)) 14%, var(--card));
  border: 0;
  box-shadow: inset 3px 0 0 var(--c, var(--cal-other-activity));
  color: var(--fg);
}
/* FullCalendar colours .fc-event-main from --fc-event-text-color with its
   own descendant rule, which an ancestor's `color` cannot reach. */
#calendar .fc-event .fc-event-main { color: var(--fg); }
/* FullCalendar rings an overlapping (inset) timegrid event in the page
   colour so side-by-side events stay apart; keep that ring with the edge. */
#calendar .fc-timegrid-event-harness-inset .fc-event {
  box-shadow: inset 3px 0 0 var(--c, var(--cal-other-activity)), 0 0 0 1px var(--fc-page-bg-color);
}
#calendar .fc-event.cal-state-unconfirmed,
#calendar .fc-event.cal-state-awaiting_card,
.cal-legend-swatch.cal-state-unconfirmed,
.cal-legend-swatch.cal-state-awaiting_card {
  background: var(--card);
  box-shadow: none;
  border: 1.5px dashed var(--c, var(--cal-other-activity));
}
#calendar .fc-event.cal-state-did_not_attend,
#calendar .fc-event.cal-state-late_cancellation { opacity: 0.55; }
/* On the title, not the <a>: FullCalendar's own
   "a.fc-event { text-decoration: none }" would win on the root in month
   view, and the time line should stay readable. */
#calendar .cal-state-did_not_attend .cal-event-title,
#calendar .cal-state-late_cancellation .cal-event-title { text-decoration: line-through; }

/* Keyboard focus (task 5 review): every event is focusable (tabindex 0),
   and the vendored bundle's own default here is .fc-event:focus:after, a
   black 25% overlay (--fc-event-selected-overlay-color, mapped to
   transparent above as a second line of defence) - worst on dark, and
   never our own ring. display: none removes it outright for both mouse
   and keyboard focus; :focus-visible below restates the brand's ring,
   keyboard-only, the same split #calendar .fc-button already uses.

   Fully INSET, not the site-wide outward --focus-ring: the timegrid's
   own .fc-scroller-harness clips overflow (the vertical scroll from
   issue #97), so an outward ring would clip at the grid's edges exactly
   the way .tab's ring does under 640px - see that rule's own comment.
   Confirmed events keep their coloured left edge alongside it; dashed
   (unconfirmed/awaiting card) events carry no box-shadow of their own
   normally, so their focus rule below is the ring alone. */
#calendar .fc-event:focus::after { display: none; }
#calendar .fc-event:focus-visible {
  outline: 2px solid transparent;
  box-shadow:
    inset 3px 0 0 var(--c, var(--cal-other-activity)),
    inset 0 0 0 2px var(--ring-inner),
    inset 0 0 0 4px var(--ring-outer, transparent);
}
#calendar .fc-event.cal-state-unconfirmed:focus-visible,
#calendar .fc-event.cal-state-awaiting_card:focus-visible {
  box-shadow:
    inset 0 0 0 2px var(--ring-inner),
    inset 0 0 0 4px var(--ring-outer, transparent);
}

#calendar .cal-event-body {
  height: 100%;
  padding-left: 4px;
  overflow: hidden;
}
/* Line 1 never wraps: a 50-minute block has room for two lines, and a
   wrapped time line would push the title out of the box. The full text
   is always in the event's aria-label. */
#calendar .cal-event-time {
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#calendar .cal-event-state { color: var(--warn-fg); font-weight: 600; }
#calendar .cal-event-title { font-weight: 700; font-size: 0.8rem; }

/* Month view: one line per event, time then title, clipped with an
   ellipsis rather than wrapping the day cell taller. */
#calendar .fc-daygrid-event .cal-event-body {
  display: flex;
  gap: 0.35em;
  align-items: baseline;
  min-width: 0;
  white-space: nowrap;
}
#calendar .fc-daygrid-event .cal-event-time { flex: none; }
#calendar .fc-daygrid-event .cal-event-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The calendar key, below the grid. */
.cal-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  font-size: 12.5px;
  color: var(--fg-soft);
}
.cal-legend li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.cal-legend-swatch {
  display: inline-block;
  flex: none;
  width: 16px;
  height: 11px;
  border-radius: 2px;
  box-sizing: border-box;
}
.cal-legend-swatch.cal-legend-hatch {
  background: var(--cal-hatch);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* The held-slot card inside the event dialog. Borrows the dialog's own
   rows; the expiry is stated in .note-info, because it is the one thing
   a counsellor opening this needs before they can decide anything. */
.hold-who {
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0 0 0.6rem;
}
/* Paint retired for .note-info (Task 3): the markup pairs
   class="hold-expiry note-info" so this rule only ever has to carry what
   .note-info does not — the size and the gap before whatever comes next
   (sometimes .hold-clash, with no other element between them). */
.hold-expiry {
  font-size: 0.9rem;
  margin: 0 0 0.75rem;
}
.hold-clash {
  font-size: 0.9rem;
  background: var(--warn-bg);
  border: 1px solid var(--line);
  border-left: 3px solid var(--danger-accent);
  padding: 0.45rem 0.6rem;
  margin: 0 0 0.75rem;
}
.hold-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 0.9rem;
}
.hold-actions form {
  margin: 0;
}
.hold-tells {
  color: var(--muted);
  font-size: 0.82rem;
  margin: 0.6rem 0 0;
}

/* The hover quick-confirm menu (issue #97): no existing interactive
   dropdown/menu convention in this file to match - the closest is
   .info-bubble, a hover tooltip with no clickable rows - so this follows
   the plan's fallback instead: a card surface (matching .event-detail's
   dialog language) plus the same elevation shadow .info-bubble already
   uses for a surface floating over the page. */
#calendar .cal-outcome-menu {
  position: absolute;
  z-index: 20;
  min-width: 11rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
  padding: 0.3rem;
}
#calendar .cal-outcome-menu button[role="menuitem"] {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-btn);
  color: var(--fg);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
}
#calendar .cal-outcome-menu button[role="menuitem"]:hover,
#calendar .cal-outcome-menu button[role="menuitem"]:focus-visible {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: none;
}
#calendar .cal-outcome-menu-separator {
  height: 1px;
  background: var(--line);
  margin: 0.3rem 0.2rem;
}

.event-detail {
  border: 1px solid var(--line);
  border-radius: var(--radius-panel);
  background: var(--card);
  color: var(--fg);
  max-width: 26rem;
  padding: 1.25rem 1.5rem;
}

.event-detail::backdrop {
  background: rgba(20, 18, 15, 0.35);
}

.event-detail-rows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 1rem;
  margin: 0.75rem 0;
}

.event-detail-rows dt {
  color: var(--muted);
}

.event-detail-rows dd {
  margin: 0;
  /* A grid item will not shrink below its content by default, so a long
     email pushed the booking request page wider than a phone (app#526). */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The calendar's event detail (templates/scheduling/event_detail.html) in
   the Add to diary dialog's shell: header / scrolling body / pinned
   footer, the same grid as #entry-create below. Keyed on the fragment's
   own footer with :has(), since the charge-or-waive and booking-hold
   fragments share this dialog and keep their plain layout (and the
   dialog's own Close button, which this shell replaces with its own).
   [data-detail-body] steps out of the way (display: contents) so the
   fragment's three blocks are the grid's rows. As with #entry-create,
   display: grid is scoped to [open] so a closed dialog stays hidden. */
#event-detail:has(.event-detail-actions) {
  max-width: 32rem;
  width: calc(100vw - 2rem);
  max-height: min(720px, 100dvh - 2rem);
  grid-template-rows: auto 1fr auto;
  grid-template-columns: minmax(0, 1fr);
}

#event-detail[open]:has(.event-detail-actions) { display: grid; }
#event-detail:has(.event-detail-actions) [data-detail-body] { display: contents; }
#event-detail:has(.event-detail-actions) > form[method="dialog"] { display: none; }

.event-detail-close { margin: 0; }

.event-title-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem 0.6rem;
  margin-top: 0.75rem;
}

.event-title-row h2 {
  margin: 0;
  font-size: 1.1rem;
}

/* The kind chip: the entry dialog's type chip, but a label here, not a
   control, so it keeps its resting border and arrow cursor. */
.event-kind-chip { cursor: default; }
.event-kind-chip:hover { border-color: var(--line); }

/* Same overflow trick as #entry-create's form pane: the negative margin
   widens the clip box so a focused card's ring is not cut at the edge. */
.event-detail-pane {
  min-height: 0;
  overflow-y: auto;
  display: grid;
  align-content: start;
  gap: 1.25rem;
  padding: 0.25rem 0.5rem 0.25rem;
  margin: 0 -0.5rem;
}

.event-detail-pane form { margin: 0; }

/* The Details and Payment headings: h3s (they head sections, not
   fieldsets) in the form legends' register, defined with them below. */
.event-detail-legend {
  margin-top: 0;
  font-family: inherit;
  line-height: inherit;
}

.event-outcome-ask {
  margin: -0.25rem 0 0;
  font-size: 0.9375rem;
}

/* The outcome cards: the chooser's rows (.entry-create-links below) as
   radios, each tile in its outcome's status colour. The radio covers the
   whole card, so the card is the click target and keeps native keyboard
   and required-field behaviour. */
.outcome-cards {
  display: grid;
  gap: 0.5rem;
}

.outcome-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  color: var(--fg);
  cursor: pointer;
}

.outcome-card:hover {
  border-color: var(--ring-inner);
  background: var(--raised);
}

.outcome-card input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.outcome-card:has(input:focus-visible) { box-shadow: var(--focus-ring); }

/* The Attended card on a recorded DNA or late cancellation is a link to
   the edit form, not a radio (#451): same card, no underline. */
a.outcome-card { text-decoration: none; }
a.outcome-card:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.outcome-card:has(input:checked) {
  border-color: var(--ring-inner);
  background: var(--select-wash);
  box-shadow: inset 0 0 0 1px var(--ring-inner);
}

.outcome-ok { --c: var(--ok-fg); }
.outcome-danger { --c: var(--danger-fg); }
.outcome-warn { --c: var(--warn-fg); }

.event-facts {
  display: grid;
  gap: 0.5rem;
  margin: 0;
}

.event-facts > div {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 1rem;
  align-items: center;
}

.event-facts dt {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--muted);
  font-size: 0.875rem;
}

.event-facts dt svg {
  width: 15px;
  height: 15px;
  flex: none;
}

.event-facts dd { margin: 0; }

.event-detail-section .event-detail-join { margin: 0.75rem 0 0; }
.event-detail-section .join-link { margin-left: 0; }
.event-detail-section .payline-sentence { margin-top: 0.35rem; }

.event-detail-onward {
  margin: 0;
  font-size: 0.875rem;
}

/* Delete on the left, Edit and Confirm or Close on the right. */
.entry-dialog-actions.event-detail-actions { justify-content: space-between; }
.event-detail-actions .btn-danger-text svg { width: 15px; height: 15px; }

.event-detail-actions-end {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.event-detail-actions-end form { margin: 0; }

/* The chooser's five rows (phase 5 task 1, B1): full-width cards, a
   36px tile carrying the kind's own --cal-* colour (the same per-kind
   classes as the calendar legend, apps/core/tests/test_brand.py's
   test_entry_create_chooser_tiles_use_cal_kind_tokens), a bold title
   and a muted one-line description. */
.entry-create-links {
  display: grid;
  gap: 0.5rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
}

.entry-create-links a {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  color: var(--fg);
  text-decoration: none;
  /* This rule and the prose rule (a:not([class])) tie at (0,1,1); source
     order lets THIS rule's own declarations win, but it never set
     `background`, so the prose rule's gradient-underline background-image
     kept showing through as a 2px green line across the bottom of every
     card. */
  background: none;
}

.entry-create-links a:hover {
  /* --ink (the plain-text default) is ~1.13:1 against the dark theme's
     --card (--ink-raised) — effectively invisible, and these links carry
     no underline to fall back on. --focus-ring is now a box-shadow list,
     not a colour, so this reuses --ring-inner instead — the same
     theme-aware split (ink on light, green on dark; see body > header
     above for the same-shaped bug on a focus ring) — clearing WCAG 1.4.11
     (>=3:1) in both themes (apps/core/tests/test_brand.py::
     test_entry_create_links_hover_clears_contrast_in_both_themes). A
     faint fill on top (brief: "adds a faint fill"), never the border's
     only affordance. */
  border-color: var(--ring-inner);
  background: var(--raised);
}

.entry-create-link-copy {
  display: grid;
  gap: 0.05rem;
  min-width: 0;
}

.entry-create-link-title {
  font-weight: 700;
  font-size: 0.906rem; /* 14.5px, brief */
}

.entry-create-link-desc {
  font-size: 0.8125rem; /* 13px, brief */
  color: var(--muted);
  line-height: 1.35;
}

/* The 36px chooser tile and its 22px chip twin (the form header's type
   chip below): a --cal-* colour on a 13% tint. The per-kind classes
   (.cal-kind-client_session etc, base.css's calendar legend rules) set
   --c; falling back to --muted keeps an un-kinded tile (transiently, while
   JS is still populating the chip) from rendering colourless. */
.entry-tile {
  display: grid;
  place-items: center;
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-card);
  color: var(--c, var(--muted));
  background: color-mix(in srgb, var(--c, var(--muted)) 13%, var(--card));
}

.entry-tile svg {
  width: 19px;
  height: 19px;
}

.entry-tile-sm {
  width: 22px;
  height: 22px;
  border-radius: 6px;
}

.entry-tile-sm svg {
  width: 13px;
  height: 13px;
}

/* The dialog shell's fixed header (phase 5 task 1): shared shape for the
   picker (heading + close X + slot subtitle) and the form pane (type
   chip + expand + close X, then the fragment's own relocated heading and
   date subtitle). */
.entry-dialog-head-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.entry-dialog-head-row h2 {
  margin: 0;
  font-size: 1.1rem;
}

.entry-dialog-head-actions {
  display: flex;
  align-items: center;
  gap: 0.15rem;
}

.entry-dialog-sub {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.35rem 0 0;
  color: var(--muted);
  font-size: 0.875rem;
}

.entry-dialog-sub svg {
  width: 15px;
  height: 15px;
  flex: none;
}

/* Collapses when the form pane's date subtitle has nothing to show yet
   (the toolbar "+", no slot known) - the picker's own subtitle toggles
   via [hidden] instead, since it always carries the calendar-days icon
   as a child even when its text is blank. */
.entry-dialog-sub:empty {
  display: none;
}

/* The type chip (phase 5 task 1): replaces the old Back button - it
   still carries data-entry-back, so static/js/calendar.js's existing
   wiring finds it unchanged - and shows the open form's own kind. */
.entry-type-chip {
  /* Not the 999px pill radius - that shape is reserved for status pills
     (apps/core/tests/test_brand.py::test_only_the_named_components_are_pills) -
     so this is a small rounded-rectangle control, matching the rest of
     the app's buttons. */
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-btn);
  padding: 0.2rem 0.65rem 0.2rem 0.25rem;
  font-size: 0.8125rem;
  font-weight: 600;
  background: var(--card);
  color: var(--fg);
  cursor: pointer;
}

.entry-type-chip:hover {
  border-color: var(--line-strong);
}

.entry-type-chip svg {
  width: 14px;
  height: 14px;
  color: var(--muted);
  flex: none;
}

.entry-dialog-head {
  border-bottom: 1px solid var(--line);
  padding-bottom: 0.85rem;
  margin-bottom: 0.6rem;
}

/* The entry dialog now holds a whole add form, not five links (issue #72).
   .event-detail's 26rem suits a detail panel and crushes a form, so widen
   this one dialog and let a long form scroll inside it rather than off the
   viewport.

   Phase 5 task 1 (the dialog shell): #entry-create itself is the grid -
   header auto / body 1fr / footer auto - so a long form's fixed header
   and footer never move; only the body track scrolls.
   [data-entry-picker] spans both the header and body rows as one block
   (it never needs to scroll); [data-entry-form-head] and
   [data-entry-form-pane] take one row each, and the static footer
   (.entry-dialog-actions) is the third.

   display: grid is scoped to [open], not the bare #entry-create - an
   author style always beats a user-agent one regardless of specificity,
   so an unconditional `display: grid` here would override the browser's
   own `dialog:not([open]) { display: none }` and leave the dialog
   visibly on screen (still laid out, just with nothing telling it to
   hide) after .close() removes the attribute. Measured: every Save/
   Cancel e2e test in this file hung waiting for #entry-create to become
   hidden until this was scoped. */
#entry-create {
  max-width: 32rem;
  width: calc(100vw - 2rem);
  grid-template-rows: auto 1fr auto;
  max-height: min(720px, 100dvh - 2rem);
}

#entry-create[open] {
  display: grid;
}

#entry-create [data-entry-picker] {
  grid-row: 1 / 3;
  min-height: 0;
  overflow-y: auto;
}

#entry-create [data-entry-form-head] {
  grid-row: 1;
}

/* overflow-y makes the other axis clip too, at the pane's own content-box
   edges, and the dialog's own padding sits outside this box. Fields are
   width:100% so they start flush at x=0, and the focus ring is 6px wider
   than the field on every side, so the left of it was being cut (#227).
   The negative margin cancels the padding, so the form does not drift out
   of line with the header or the footer; only the clip box grows. */
#entry-create [data-entry-form-pane] {
  grid-row: 2;
  min-height: 0;
  overflow-y: auto;
  padding: 0 0.5rem;
  margin: 0 -0.5rem;
}

/* Cancel and Save, neither of them scrolling (#227); Back used to share
   this footer but is now the header's type chip (phase 5 task 1). */
.entry-dialog-actions {
  grid-row: 3;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}

.entry-dialog-actions form { margin: 0; }

/* The "more below" cue (#316): the pinned footer alone left a long form
   looking finished wherever its fold happened to fall between sections.
   static/js/calendar.js sets data-more-below while the pane has content
   past its bottom edge, and the last 4rem fade out until the end is
   reached. A mask, not an overlay, so nothing sits on top of the fields
   to catch a click. */
#entry-create[data-more-below] [data-entry-form-pane] {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 4rem), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 4rem), transparent);
}

/* On a phone the dialog becomes a full-screen sheet (#316): the floating
   box's 1rem gutter all round bought nothing but a smaller scroll area,
   and Cancel and Save split the footer so both sit under a thumb. The
   width and height are restated in full because the UA's modal-dialog
   max-width/max-height (calc(100% - 2em - 6px)) and the rule above's
   min(720px, ...) would otherwise still apply. */
@media (max-width: 600px) {
  #entry-create,
  #event-detail:has(.event-detail-actions) {
    width: 100%;
    max-width: none;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border: 0;
    border-radius: 0;
    padding: 1rem 1rem calc(0.85rem + env(safe-area-inset-bottom));
  }

  .entry-dialog-actions > form,
  .entry-dialog-actions > button { flex: 1 1 0; }

  .event-facts > div { grid-template-columns: 7.5rem minmax(0, 1fr); }

  .entry-dialog-actions button { width: 100%; }
}

/* The Payment fieldset (#243, R9) is one of the client session form's
   sections since phase 5 task 2: unboxed, with the same small uppercase
   legend as When, How and Notes. Task 3 builds the payment line inside
   it. */
.entry-pay {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
  display: grid;
  gap: 0.75rem;
}

/* ---- The client session form body (phase 5 task 2, brief §2.5, §6.2) ----
   templates/logbook/_session_form_body.html: the client, then When, How,
   Payment, What happened and Notes as unboxed fieldsets whose legends are
   small, tracked, muted headings. Every row is a .field: label above,
   help below, one field width (fields fill their column; Date, Start and
   Length share a wrapping row). */
.session-form-body {
  display: grid;
  gap: 1.25rem;
  max-width: 34rem;
  margin-bottom: 1.25rem;
}

.form-section {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
  display: grid;
  gap: 0.75rem;
}

/* Brief §2.5 asks for small uppercase muted legends; BRAND.md bans
   uppercasing (test_no_uppercase_transform), so these take
   .fc-label's register instead - Karla 700, small and tracked - in
   --muted, sentence case. */
.form-section > legend,
.entry-pay > legend,
.event-detail-legend {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--muted);
  padding: 0;
  margin-bottom: 0.6rem;
}

/* minmax(0, 1fr): a grid item's automatic minimum is its min-content, and
   Chrome's date input has a wide one, so without this Date spilled into
   Start's cell. */
.session-form-body .field {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.3rem;
  align-content: start;
  min-width: 0;
  margin: 0;
}

.session-form-body .field > label:not(.check-row) {
  font-size: 0.844rem; /* 13.5px */
  font-weight: 700;
}

.session-form-body .helptext { margin-top: 0; max-width: none; }

.session-form-body input:not([type="checkbox"]):not([type="radio"]),
.session-form-body select,
.session-form-body textarea { max-width: none; }

/* The segmented control keeps its own width inside a stretching row. */
.session-form-body .segmented[role="radiogroup"] { justify-self: start; }

.entry-when-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.6rem;
}

/* The When fieldset's own track is pinned to its width (#526): left as an
   implicit auto track it took the row's intrinsic width, two 177px
   columns, and at 375px the page scrolled sideways. */
.session-form-body [data-entry-when] { grid-template-columns: minmax(0, 1fr); }

/* B3: the preset select over the minutes box, which entry-form.js only
   shows for "Other length". */
.length-field {
  display: grid;
  gap: 0.4rem;
}

/* The live "18:00-18:50 (50 min)" readout, with Repeats inline at the
   right. A tint of the text colour rather than --raised, which equals
   --paper in light theme and would vanish on the full pages. */
.entry-readout {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: 0.875rem;
  background: color-mix(in srgb, var(--fg) 5%, transparent);
  border-radius: var(--radius-btn);
  padding: 0.45rem 0.65rem;
}

.entry-readout > svg {
  width: 16px;
  height: 16px;
  flex: none;
  color: var(--muted);
}

.entry-readout > b { font-variant-numeric: tabular-nums; }

.session-form-body .entry-readout > [data-row="repeats"] {
  margin-left: auto;
  max-width: 100%;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 0.4rem;
}

.session-form-body .entry-readout > [data-row="repeats"] > label {
  font-size: 0.844rem;
  font-weight: 600;
  color: var(--muted);
}

/* min-width: 0 lets a long "Every 2nd Tuesday..." option shrink on a
   phone rather than push the page wider (#526). */
.session-form-body .entry-readout > [data-row="repeats"] > select {
  width: auto;
  min-width: 0;
  max-width: 100%;
  padding: 0.2rem 0.4rem;
}

.entry-readout > [data-row="repeats"] > .errorlist,
.entry-readout > [data-row="repeats"] > .entry-repeats-note {
  grid-column: 1 / -1;
}

/* The payment line (phase 5, B7/B8): line 1 a card icon, the summary and
   a pill; line 2 the consequence sentence (muted, a neutral aside about
   what happens next); line 3 the Starter note or the method select.
   entry-form.js keeps lines 1 and 2 live. */
.payline-summary {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  font-size: 0.9375rem;
}

.payline-summary > svg {
  width: 18px;
  height: 18px;
  flex: none;
  color: var(--muted);
}

.payline-sentence,
.payline-plan {
  margin: 0;
  font-size: 0.875rem;
  color: var(--muted);
}

/* M1: the edit form's client, fixed once the session exists, reads as a
   value, not as a greyed-out control. */
.field-readonly .field-label {
  font-size: 0.844rem;
  font-weight: 700;
}

.field-readonly .readonly-value {
  margin: 0;
  font-weight: 600;
}

/* ---- At-a-glance dashboard widgets ---------------------------------------
   Slice 9: /dashboard/ is a 2×2 widget grid (stacked on small viewports).
   Your Day reuses the calendar --cal-* event tokens: future events solid
   (white text, matching --fc-event-text-color), past events outlined. */

/* The cards flow, in reading order, into two balanced columns (#710). The
   columns used to be fixed in each template, so a qualified counsellor on a
   course got two cards on the left and four on the right. A grid of cells
   aligns its rows and strands a gap beside a tall card, so it's a
   multi-column box: whole cards only, spaced by their own bottom margin. */
.dashboard-grid {
  columns: 2;
  column-gap: 1rem;
  margin-top: 1rem;
}

.dashboard-grid > *,
.dashboard-grid.is-pinned > .dashboard-column > * {
  break-inside: avoid;
  margin: 0 0 1rem;
}

/* The live poll can change Your Day's height, and in a balanced box that
   moves cards between columns while someone reads them. So before the first
   swap, static/js/dashboard-live.js pins each card into the column it sits
   in: two plain stacks, and a taller Your Day only lengthens its own. */
.dashboard-grid.is-pinned {
  columns: auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 1rem;
  align-items: start;
}

.dashboard-grid.is-pinned > .dashboard-column { margin: 0; min-width: 0; }

@media (max-width: 900px) {
  .dashboard-grid { columns: auto; }
  .dashboard-grid.is-pinned { grid-template-columns: 1fr; }
}

.widget {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1rem 1.15rem 1.15rem;
}

.widget-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.widget-header h2 {
  margin: 0;
  font-size: 1.05rem;
}

.widget-header-link { font-size: 0.85rem; white-space: nowrap; }

.widget-subtitle {
  color: var(--muted);
  font-size: 0.85rem;
  margin: 0.15rem 0 0.75rem;
}

.widget-empty { color: var(--muted); font-size: 0.9rem; }

.widget-rows { margin: 0.75rem 0 0; }

.widget-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.35rem 0;
}

.widget-row dt { color: var(--muted); font-size: 0.9rem; }

.widget-row dd { margin: 0; font-weight: 600; font-size: 0.9rem; text-align: right; }

.widget-row dd .pill { margin-left: 0.4rem; }

.widget-note { color: var(--muted); font-weight: 400; }

.widget-untracked { color: var(--muted); font-weight: 400; }

.widget-milestone {
  font-weight: 600;
  color: var(--fg);
  font-size: 0.85rem;
  margin: 0.5rem 0 0;
}

/* ---- Dashboard charts ---------------------------------------------------
   Server-rendered inline SVG; there is no charting library, so these
   classes are the whole styling surface. Geometry comes from
   apps/enrolments/services/dashboard_charts.py.

   Mark specs, held across every chart: 2px lines with round caps, markers
   r >= 4 carrying a 2px ring in the surface colour, area washes at 10%,
   and gridlines that are SOLID hairlines. A dash means a genuine
   threshold - a required pace, a course maximum - never a grid, because a
   dashed grid reads as a projection that isn't there.

   Text never wears the data colour: values and labels take the ink
   tokens, and identity comes from the coloured mark beside them. */
:root {
  /* Graph option B (#301, James): the card stays white, the line is navy
     and the shading pale mint. The line is --ok-fg rather than --accent
     (both ink on light) because it is data, not a control, and it turns
     neon in the dark theme. The shading and the "inside your course
     maximum" band are the green select wash: a tint of the navy line would
     be grey, and the mint is the graph's one touch of brand green. */
  --chart-series: var(--ok-fg);
  --chart-ghost: color-mix(in srgb, var(--muted) 45%, transparent);
  --chart-wash: var(--select-wash);
  --ok-band: var(--select-wash);
  --risk-band: color-mix(in srgb, var(--danger-fg) 10%, transparent);
}

.chart {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 0.75rem;
  overflow: visible;
}

.chart-grid {
  stroke: var(--line);
  stroke-width: 1;
}

.chart-axis-label {
  fill: var(--muted);
  font-size: 10px;
  font-weight: 500;
}

.chart-value-label {
  fill: var(--fg);
  font-size: 11.5px;
  font-weight: 700;
}

.chart-line {
  fill: none;
  stroke: var(--chart-series);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.chart-wash { fill: var(--chart-wash); }

.chart-mean {
  fill: none;
  stroke: var(--chart-ghost);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Marks carry a 2px ring in the surface colour so they stay legible where
   they cross a line or each other - never a border to separate them. */
.chart-point {
  fill: var(--chart-series);
  stroke: var(--card);
  stroke-width: 2;
}

/* The forecast, deliberately subordinate to the actual line: it is a
   claim about the future and will sometimes be wrong. */
.chart-pace,
.chart-projection {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 5 4;
}

.chart-pace { stroke: var(--chart-ghost); }
.chart-projection { stroke: var(--chart-series); stroke-dasharray: 4 4; }

.chart-projection-end {
  fill: var(--card);
  stroke-width: 2;
}

.chart-projection-end.is-short { stroke: var(--danger-fg); }
.chart-projection-end.is-enough { stroke: var(--ok-fg); }

.chart-caption {
  color: var(--muted);
  font-size: 0.8rem;
  margin: 0.5rem 0 0;
}

.chart-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.1rem 1rem;
  margin: 0.55rem 0 0;
  padding: 0;
  font-size: 0.78rem;
  color: var(--muted);
}

.chart-legend li { display: flex; align-items: center; gap: 0.4rem; }

.chart-key {
  width: 12px;
  height: 12px;
  border-radius: var(--radius-sq);
  flex: none;
}

.chart-key-line { height: 2px; width: 14px; background: var(--chart-series); border-radius: 1px; }
.chart-key-mean { height: 2px; width: 14px; background: var(--chart-ghost); border-radius: 1px; }
.chart-key-dash { height: 0; width: 14px; border-top: 2px dashed var(--chart-ghost); }
.chart-key-ghost { background: var(--chart-ghost); }
.chart-key-ok { background: var(--ok-band); }
.chart-key-risk { background: var(--risk-band); }

/* Every chart ships a table-view twin, so no value is reachable only by
   reading a mark. */
.table-view { margin: 0.7rem 0 0; }
.table-view summary { font-size: 0.8rem; color: var(--muted); cursor: pointer; }
.table-view table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.45rem;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}
.table-view th, .table-view td {
  text-align: left;
  padding: 0.25rem 0.1rem;
  border-bottom: 1px solid var(--line);
  font-weight: 500;
}
.table-view th { color: var(--muted); }
.table-view td { text-align: right; font-weight: 600; }

/* ---- Supervision range bar ---------------------------------------------
   Scale is client hours per supervision hour: lower is better, so the
   region beyond the course maximum sits to the right. The bands are
   separated by a 2px gap in the surface, not by a stroke. */
.band-ok { fill: var(--ok-band); }
.band-risk { fill: var(--risk-band); }

.bar-limit { stroke: var(--danger-fg); stroke-width: 2; }

.bar-limit-label {
  fill: var(--danger-fg);
  font-size: 10.5px;
  font-weight: 600;
}

.bar-mark {
  fill: var(--ok-fg);
  stroke: var(--card);
  stroke-width: 2;
}

.bar-mark.is-breached { fill: var(--danger-fg); }

.bar-mark-predicted {
  fill: var(--card);
  stroke: var(--chart-ghost);
  stroke-width: 2;
}

.bar-mark-label {
  fill: var(--fg);
  font-size: 13px;
  font-weight: 700;
}

.bar-undefined {
  fill: var(--muted);
  font-size: 12px;
  font-weight: 700;
}

/* ---- Headline tiles -----------------------------------------------------
   Four or five numbers answering "am I OK?" above the fold; 170px lets
   five (the qualified row, #264) share one desktop line. Values use the font's
   proportional figures: tabular-nums equalises digit widths and makes a
   large standalone number look loose. */
.tile-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 1rem;
  margin: 1rem 0 1.25rem;
}

.tile {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.85rem 1rem 0.95rem;
}

.tile-label {
  font-size: 0.78rem;
  color: var(--muted);
  margin: 0 0 0.15rem;
}

.tile-value {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600;
  font-size: 1.8rem;
  line-height: 1.05;
  letter-spacing: -0.02em;
  margin: 0;
}

.tile-unit {
  font-family: 'Karla Variable', ui-sans-serif, system-ui, sans-serif;
  font-size: 0.92rem;
  font-weight: 500;
  color: var(--muted);
  margin-left: 0.2rem;
}

/* Status is an icon plus a word - never colour alone. */
.tile-status {
  font-size: 0.78rem;
  margin: 0.25rem 0 0;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--muted);
}

.tile-status-ok { color: var(--ok-fg); font-weight: 600; }
.tile-status-warn { color: var(--warn-fg); font-weight: 600; }
.tile-status-danger { color: var(--danger-fg); font-weight: 600; }
.tile-status-muted { color: var(--muted); }

.tile-dot {
  width: 9px;
  height: 9px;
  border-radius: var(--radius-sq);
  flex: none;
}

/* The Next event square (#95): urgency, not event kind. --ok-fg, not raw
   --green - a solid --green fill is 1.19:1 on paper and has no boundary
   there (see .progress-fill's own comment above for the same fix). Soon
   and imminent are a heads-up, so an open red square (#301: a heads-up is
   an outline); now is the filled brand red. The label beside it says the
   same thing in words. */
.tile-dot[data-urgency] { background: var(--ok-fg); }
.tile-dot[data-urgency="soon"],
.tile-dot[data-urgency="imminent"] { background: transparent; box-shadow: inset 0 0 0 2px var(--warn-fg); }
.tile-dot[data-urgency="now"] { background: var(--danger-accent); }
.tile-dot[data-urgency="imminent"],
.tile-dot[data-urgency="now"] { animation: urgency-pulse 1s ease-in-out infinite alternate; }
@keyframes urgency-pulse { to { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) {
  .tile-dot[data-urgency] { animation: none; }
}

.tile-spark { margin-top: 0.5rem; }

.spark-line {
  fill: none;
  stroke: var(--chart-ghost);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.spark-end { fill: var(--chart-series); }

/* A chart inside a widget, under its own small heading. */
.widget-chart { margin-top: 1rem; }

.widget-chart-title {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: 0.9rem;
  color: var(--muted);
  margin: 0;
}

.widget-chart-value { font-weight: 600; color: var(--fg); }

/* Clients index (issue #76) --------------------------------------------- */

.client-stats {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0 2.25rem;
  margin: 0;
  padding: 0;
}

.client-stat-value {
  font-size: 1.35rem;
  font-weight: 600;
}

.client-stat-label { color: var(--muted); font-size: 0.85rem; }

/* The whole row is the target, so the hit area matches what it looks like.
   The spans live inside the anchor now, so the .client-list weight/muted
   rules above no longer reach them — restated here rather than widened
   there, because the other lists still use the originals. */
.client-link {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
  width: 100%;
  text-decoration: none;
  color: inherit;
}

.client-link > span:first-child { font-weight: 600; }
.client-link > span:not(:first-child) { color: var(--muted); }

.client-link:hover .client-alias { text-decoration: underline; }

.client-code { color: var(--muted); font-size: 0.8rem; }
.client-name { color: var(--muted); }

.client-hours { margin-left: auto; font-variant-numeric: tabular-nums; }

/* Added / Ended dates (issue #30): muted from the rule above; flex-wrap
   drops them to their own line at phone width. */
.client-dates { font-size: 0.8rem; }

/* Client record (issue #76) --------------------------------------------- */

.client-facts, .client-totals {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.9rem;
  color: var(--muted);
  margin: 0 0 0.4rem;
}

/* A booking, not evidence: outlined, never the filled treatment a
   completed session earns (BRAND.md, green is a fill). Radius matches
   .session-outcome-flag and the other status chips — nothing in this
   brand is a pill. */
.session-booked-flag {
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.05rem 0.5rem;
  border-radius: var(--radius-sq);
}

/* Client record (issue #159): the tag group sits flush right of the row's
   facts, same as .agenda-list li > a below, and the Edit link that follows
   it keeps its own left margin rather than inheriting the group's. */
.session-list .session-tags {
  margin-left: auto;
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  justify-content: flex-end;
}
.session-list li > .session-tags + a { margin-left: 0; }

.client-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin: 0 0 0.75rem;
}

.client-filter input { min-width: 14rem; }

.client-count { margin-left: auto; color: var(--muted); font-size: 0.85rem; }

/* Payments page (issue #174): tiles as the dashboard; the list is the
   record-row recipe with a tabular amount column; payouts is a small dl.
   The base .payment-list li is the client record's five cells (date,
   client-or-kind, amount, chip, an optional receipt link with no
   placeholder); .payment-list-month adds the sixth cell the Payments
   page's row carries (alias plus name, so kind moves to its own column). */
.page-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 0.75rem 0 0; }
.page-actions .month { font-weight: 600; }
.page-actions .month-nav { display: inline-flex; gap: 0.25rem; }
.page-actions .btn-secondary { font-size: 0.85rem; padding: 0.3rem 0.7rem; }
.page-actions > .btn-secondary:last-child { margin-left: auto; }
.payment-list { list-style: none; padding: 0; margin: 0 0 0.75rem; }
.payment-list li { display: grid; grid-template-columns: 6.5rem 1fr 4.5rem auto auto; gap: 0.9rem; align-items: baseline; padding: 0.45rem 0.1rem; border-bottom: 1px solid var(--line); font-size: 0.92rem; }
/* The sixth cell used to be a fixed 5.5rem, sized for a 0.85rem "Receipt"
   link (#243 review, fix 1): too narrow for the settle form's button, or
   a select beside it on a card row. auto matches the plain .payment-list
   above, which already lets its own last two cells size to content. */
.payment-list-month li { grid-template-columns: 6.5rem 1fr 9.5rem 4.5rem 8.5rem auto; }
.payment-list li:last-child { border-bottom: none; }
.payment-list .payment-client { font-weight: 600; }
.payment-list .payment-client small { font-weight: 400; color: var(--muted); }
.payment-list .payment-what, .payment-list .payment-date { color: var(--muted); }
.payment-list .payment-amount { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
.payment-list .payment-amount.is-zero { color: var(--muted); font-weight: 400; }
.payment-list li > a { font-size: 0.85rem; }
.payouts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.75rem 1.5rem; margin: 0.5rem 0 0.25rem; }
.payouts div { display: flex; flex-direction: column; gap: 0.1rem; }
.payouts dt { color: var(--muted); font-size: 0.8rem; }
.payouts dd { margin: 0; font-weight: 600; font-size: 1.05rem; font-variant-numeric: tabular-nums; }
.payouts-note { color: var(--muted); font-size: 0.85rem; margin: 0.5rem 0 0; }
.stripe-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; margin: 0 0 1rem; font-size: 0.92rem; }
.settings-note { color: var(--muted); font-size: 0.85rem; margin: 0.25rem 0 0.85rem; }
.inline-form { display: inline; margin: 0; }

/* The settle form (#243 review, fix 1): a button, and on a card row a
   select beside it, in a cell that had no rule of its own beyond
   .inline-form's bare `display: inline`. Wraps rather than overflows.
   `.settle-method` overrides the global select's `width: 100%; max-width:
   26rem` (a field made for a form column, not an inline control) the
   same way `.week-row select` and `.checklist-item-form select` already
   do. Comes after .inline-form so its own display wins at equal
   specificity. */
.settle-form {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.35rem;
}
.settle-method {
  width: auto;
  max-width: none;
  padding: 0.2rem 0.4rem;
  font-size: 0.8rem;
}
@media (max-width: 800px) { .payment-list li { grid-template-columns: 1fr 1fr; } }

.settings-form .dashboard-section { margin-top: 0; }

/* Side-by-side pairs (first/last name, start/end times). Wraps on
   narrow screens; the pair together respects the 26rem field cap. */
.settings-cols {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1rem;
  max-width: 26rem;
}

.settings-cols > .form-row { flex: 1 1 10rem; }

/* ---- Membership and qualifications (#320) ------------------------------
   One page, two lists of bordered blocks (templates/enrolments/
   memberships.html). The blocks toggle [hidden] from memberships.js. */
.memberships-form { max-width: 40rem; }
/* The Settings tab's second list, a step down from its h2. */
.settings-membership h3 { font-size: 0.95rem; margin: 1.25rem 0 0.5rem; }
.memberships-section { padding-top: 1.25rem; margin-top: 1.5rem; border-top: 1px solid var(--line); }
.memberships-section:first-of-type { padding-top: 0; margin-top: 0; border-top: 0; }
.memberships-section h2 { font-size: 1.15rem; margin: 0 0 0.25rem; }
.memberships-hint { font-size: 0.9rem; color: var(--muted); margin: 0 0 1rem; max-width: 32rem; }
.memberships-blocks { display: grid; gap: 0.75rem; }
.memberships-block {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1rem;
}
.memberships-block > .form-row:last-child { margin-bottom: 0; }
.memberships-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 0.85rem;
}
.memberships-pair input,
.memberships-pair select { max-width: none; }
@media (max-width: 34rem) { .memberships-pair { grid-template-columns: 1fr; } }
/* Remove sits in the block's top-right corner, clear of the first label. */
.memberships-remove,
.memberships-remove-button {
  position: absolute;
  top: 0.75rem;
  right: 1rem;
  font-size: 0.85rem;
  color: var(--danger-fg);
}
.memberships-block > .memberships-pair:first-of-type { padding-right: 4.5rem; }
@media (max-width: 34rem) { .memberships-block > .memberships-pair:first-of-type { padding-right: 0; padding-top: 1rem; } }
/* The renewal nudge while the date is blank. The wash and the stripe
   carry the warn level, the copy stays --fg (as .messages li.warning). */
.memberships-nudge {
  margin: -0.35rem 0 0.85rem;
  padding: 0.35rem 0.6rem;
  border-radius: var(--radius-btn);
  background: var(--warn-bg);
  border: 1px solid var(--line);
  border-left: 3px solid var(--danger-accent);
  color: var(--fg);
  font-size: 0.85rem;
}
/* "+ Add another" and Remove read as links, not buttons (#320's brief):
   neither is the page's action, and a second filled button beside Save
   would compete with it. Scoped here because .link-button's own rule lives
   in the websites stylesheet, which this page does not load. */
.memberships-form .link-button,
.memberships-form .link-button:hover {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg);
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
}
.memberships-form .memberships-remove-button,
.memberships-form .memberships-remove-button:hover { color: var(--danger-fg); font-size: 0.85rem; }
.memberships-form .memberships-add { margin-top: 0.75rem; font-weight: 600; }
.memberships-actions { display: flex; align-items: center; gap: 1.5rem; margin-top: 1.5rem; }

/* Checkbox/radio groups render as fieldset+legend for the group name;
   strip the box chrome and let the legend look like a row label. */
.form-row > fieldset {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.form-row > fieldset > legend {
  padding: 0;
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0.2rem;
}

/* The Business address group (#305, forms/_practice_fields.html): five
   ordinary rows under one legend, unboxed in the wizard and in Settings
   alike. Element plus class so it out-ranks .settings-form fieldset and
   .settings-form legend, which box every other Settings fieldset. */
fieldset.address-fields {
  border: 0;
  padding: 0;
  margin: 0 0 0.85rem;
  min-width: 0;
}

fieldset.address-fields > legend {
  padding: 0;
  font-size: 0.9rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.settings-actions { margin: 1.25rem 0 0; }

.settings-account-email {
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0 0 0.75rem;
}

/* Label-and-value rows, not the .payouts stat grid: these are facts about
   the account and one of them ends in a button (#227). */
.account-facts { margin: 0 0 1rem; display: grid; gap: 0.6rem; }
.account-facts > div {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 1rem;
}
.account-facts dt { color: var(--muted); font-size: 0.85rem; min-width: 8rem; }
.account-facts dd { margin: 0; font-weight: 600; }
.account-facts dd form { margin: 0; }

/* The "this entry is in the past" warning and the reason the repeat
   cadence is disabled (issue #79). Both are built by entry-form.js, so
   no template carries them. */
.entry-past-banner {
  /* A heads-up, as .messages li.warning (#301). */
  background: var(--warn-bg);
  border: 1px solid var(--line);
  border-left: 3px solid var(--danger-accent);
  color: var(--fg);
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius-card);
  margin: 0 0 0.75rem;
  font-size: 0.92rem;
}

.entry-repeats-note {
  display: block;
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 0.2rem;
}

/* ── Journal (issue #94) ───────────────────────────────────────────────
   The index (tiles, search/sort, entry rows, pagination), the detail
   page and the writing page. Reference mock: .dev/journal-mockup.html.
   Everything rides the role tokens, so both themes come free. */

/* Button variants (brief §2.1). Qualified with the tag so they beat the
   base `button` and `.btn` fills. */
a.btn-secondary, button.btn-secondary, summary.btn-secondary {
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line-strong);
}
a.btn-secondary:hover, button.btn-secondary:hover, summary.btn-secondary:hover {
  background: var(--raised);
}

/* Disabled (the sign-up code page's resend countdown, #364): no hover. */
button.btn-secondary:disabled,
button.btn-secondary:disabled:hover {
  background: var(--card);
  color: var(--muted);
  border-color: var(--line);
  cursor: default;
}

/* The sign-up code page (#364): Continue beside "Send a new code", and
   "Wrong address? Change it" as a prose link that posts. */
.code-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.code-aside { margin-top: 1.5rem; font-size: 0.92rem; color: var(--muted); }
.code-aside .link-button,
.code-aside .link-button:hover {
  font: inherit;
  font-weight: 600;
  padding: 0;
  border: 0;
  background: none;
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--link-line);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
  text-decoration-skip-ink: none;
  cursor: pointer;
}

/* Filled danger: only the confirm button inside a destructive confirmation. */
a.btn-danger, button.btn-danger {
  background: var(--danger-fg);
  color: var(--on-danger);
  border: 1px solid var(--danger-fg);
}
a.btn-danger:hover, button.btn-danger:hover { background: var(--danger-hover); }

/* The entry point to a destructive action: red text, no fill. Weight 600
   because --danger-fg is only just over 4.5:1 on the dark dialog. */
a.btn-danger-text, button.btn-danger-text {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: transparent;
  color: var(--danger-fg);
  border: 1px solid transparent;
  padding-inline: 0.25rem;
  font-weight: 600;
}
/* .btn.btn-danger-text on the selector list (not just a./button.) so a
   `<button class="btn btn-danger-text">` — the real markup in
   apps/booking/templates/booking/requests/detail.html and
   templates/logbook/journal_detail.html — beats `.btn:hover`'s own
   `background: var(--accent-hover)` on specificity, not just source
   order: neither the base rule above nor a./button. here ever set
   `background`, so `.btn:hover`'s ink fill was the only rule painting it,
   hiding the red label against an ink button on hover. */
a.btn-danger-text:hover, button.btn-danger-text:hover, .btn.btn-danger-text:hover {
  background: transparent;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Icon-only buttons (close, expand). Must carry an aria-label. */
a.btn-icon, button.btn-icon {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: transparent;
  color: var(--muted);
  border: 0;
}
a.btn-icon:hover, button.btn-icon:hover { background: var(--raised); color: var(--fg); }
a.btn-icon svg, button.btn-icon svg { width: 18px; height: 18px; }

/* Tabs (brief §3): switching between views. The selected tab is an ink
   underline plus bold, so selection never rests on colour or weight
   alone. Works on buttons (aria-pressed), links (aria-current) and ARIA
   tabs (aria-selected). */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
  margin: 0 0 1rem;
  border-bottom: 1px solid var(--line);
}

.tab {
  padding: 0.45rem 0 0.55rem;
  margin-bottom: -1px;
  border: 0;
  border-bottom: 3px solid transparent;
  border-radius: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.tab:hover { background: none; color: var(--fg); }

.tab[aria-pressed="true"],
.tab[aria-current="page"],
.tab[aria-selected="true"] {
  color: var(--fg);
  font-weight: 700;
  border-bottom-color: var(--fg);
}

/* A row with more tabs than a phone's width fits (Settings' nine) would
   otherwise wrap to several rows, disconnecting the selected tab's
   underline from .tabs's own base border. Scroll horizontally in one row
   instead; settings-tabs.js scrolls the pressed tab into view on load.

   overflow-x: auto alone forces the UA to compute overflow-y as auto too
   (the spec's own rule for a box where only one axis is non-visible), so
   this row would otherwise become a vertical scroll container as well as
   a horizontal one - two review-fix bugs followed from that: the global
   :focus-visible ring (--focus-ring, an OUTWARD box-shadow reaching 7px
   past the border box) got clipped top and bottom (WCAG 2.4.7), and
   .tab's margin-bottom: -1px (the desktop trick that overlaps a tab's own
   border-bottom over .tabs's 1px base line so the two never show as a
   double edge) made the row's content 1px taller than its box, so it
   could scroll vertically by that 1px. overflow-y: hidden shuts the
   vertical scroll off explicitly; .tab's own :focus-visible ring becomes
   fully INSET here so nothing it paints ever extends past its own border
   box for an ancestor's overflow to clip; and the base line moves off
   .tabs's border-bottom (which occupies layout space) onto an inset
   box-shadow instead (which doesn't), with .tab's negative margin dropped
   now that there's nothing left for it to compensate for. The selected
   tab's own opaque border-bottom still paints in front of that inset line
   (children stack above their parent's own background/box-shadow), so it
   still visually covers the base line under the selected tab exactly as
   before. */
@media (max-width: 640px) {
  .tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: thin;
    border-bottom: none;
    box-shadow: inset 0 -1px 0 0 var(--line);
    /* Tabs carry their own side padding below, so the row gap shrinks to
       keep the same spacing. */
    gap: 0 0.5rem;
  }
  /* Side padding keeps the inset focus ring off the label's first and
     last letters. Always on, not only on :focus-visible, so moving focus
     never shifts the row. */
  .tab { white-space: nowrap; margin-bottom: 0; padding-inline: 0.4rem; }
  .tab:focus-visible {
    outline: 2px solid transparent;
    box-shadow: inset 0 0 0 2px var(--ring-inner), inset 0 0 0 4px var(--ring-outer, transparent);
  }
}

/* Segmented control (brief §2.5): 2 to 4 mutually exclusive choices as
   joined buttons. The selected segment takes the accent pair. */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  max-width: 100%;
  border: 1px solid var(--field-border);
  border-radius: var(--radius-btn);
  overflow: hidden;
}

.segment {
  padding: 0.45rem 0.9rem;
  border: 0;
  border-radius: 0;
  background: var(--card);
  color: var(--fg);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
}

.segment + .segment,
.segmented label + label .segment { border-left: 1px solid var(--field-border); }

.segment:hover { background: var(--raised); color: var(--fg); }

.segment[aria-pressed="true"],
.segmented input:checked + .segment {
  background: var(--accent);
  color: var(--on-accent);
}

.segmented label { position: relative; }
.segmented input { position: absolute; opacity: 0; inset: 0; margin: 0; }
.segmented:has(input:focus-visible) { outline: 2px solid transparent; box-shadow: var(--focus-ring); }

/* Disclosures that reveal an inline form (Add client, Log supervision...)
   read as Secondary buttons (brief §2.1), with a chevron that turns open. */
details.log-form > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 1.1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-btn);
  background: var(--card);
  color: var(--fg);
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-size: 0.92rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

details.log-form > summary::-webkit-details-marker { display: none; }

details.log-form > summary::after {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}

details.log-form[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
details.log-form > summary:hover { background: var(--raised); }

@media (prefers-reduced-motion: reduce) {
  details.log-form > summary::after { transition: none; }
}

/* "Additional details (optional)": the fields most people won't have to
   hand, tucked behind one disclosure inside a form (issue #314). The label
   is the house .link rather than a button so it doesn't compete with the
   form's own submit; the chevron is details.log-form's. Open, the fields
   hang off a rule so they read as one group. */
details.form-more { margin: 0.25rem 0 1.25rem; }
details.form-more > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 600;
  font-size: 0.92rem;
  cursor: pointer;
  list-style: none;
}
details.form-more > summary::-webkit-details-marker { display: none; }
details.form-more > summary::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}
details.form-more[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
details.form-more[open] { padding-left: 1rem; border-left: 2px solid var(--line); }
.form-more-hint { color: var(--muted); font-size: 0.85rem; margin: 0.35rem 0 1rem; }
/* A low-key "show more" for something few people need (the plan step's
   "Been given a code?", #687): a small prose link, not a heading. */
details.form-more-quiet > summary { font-weight: 400; font-size: 0.88rem; }
details.form-more-quiet > summary::after { width: 0.32rem; height: 0.32rem; }
details.form-more-quiet .btn-compact { margin-top: 0.25rem; }
@media (prefers-reduced-motion: reduce) {
  details.form-more > summary::after { transition: none; }
}

/* Inline destructive confirmation (brief §2.6). A [data-confirm-open]
   trigger swaps its [data-confirm-scope]'s children for this, in place —
   see static/js/confirm-bar.js. Proved on the website builder's section
   Remove (the app's last native confirm(), via htmx's hx-confirm). */
.confirm-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius-card);
  background: var(--danger-bg);
  color: var(--fg);
}

.confirm-bar-text { flex: 1 1 12rem; margin: 0; font-size: 0.9rem; }
.confirm-bar form { margin: 0; }

/* Status pills (brief §2.4). One of five kinds. Tinted, borderless, a 6px
   dot in the status colour. The text takes the status colour only where it
   measures 4.5:1 on the pill's own wash (test_pill_text_clears_its_own_wash);
   elsewhere the text is --fg and the dot alone carries the colour.

   Wraps rather than overflows when its container is narrower than the
   label (issue found in review of #2: the payments monthly list's fixed
   8.5rem pill column, but this is a general property of the component,
   not a fix for one grid track — a by-hand payment label like "Paid £60
   by bank transfer" is longer than most callers plan a column for).
   align-items is flex-start rather than center so the dot stays pinned to
   the FIRST line rather than drifting to the vertical centre of a
   multi-line label; ::before's margin-top re-centres it on that first
   line by hand (line-height 1.3 * 12px font-size = 15.6px line box, dot
   is 6px, (15.6 - 6) / 2 = 4.8px = 0.3rem) — chosen so a single-line pill
   renders pixel-identical to the old align-items: center. */
.pill {
  display: inline-flex;
  align-items: flex-start;
  gap: 6px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 0.75rem; /* 12px */
  font-weight: 700;
  line-height: 1.3;
  white-space: normal;
  max-width: 100%;
  color: var(--pill-fg, var(--fg));
  background: var(--pill-bg);
  box-shadow: var(--pill-line, none);
}

.pill::before {
  content: "";
  flex: none;
  width: 6px;
  height: 6px;
  margin-top: 0.3rem;
  border-radius: 50%;
  background: var(--pill-dot, currentColor);
}

.pill-ok { --pill-fg: var(--ok-fg); --pill-bg: var(--select-wash); }
.pill-warn { --pill-fg: var(--warn-fg); --pill-bg: var(--warn-bg); --pill-line: inset 0 0 0 1px var(--warn-fg); }
.pill-danger { --pill-fg: var(--danger-fg); --pill-bg: var(--danger-bg); }
.pill-info { --pill-fg: var(--fg); --pill-dot: var(--info-fg); --pill-bg: var(--info-bg); }
.pill-neutral { --pill-fg: var(--fg-soft, var(--fg)); --pill-dot: var(--muted); --pill-bg: var(--raised); }

:root[data-theme="dark"] .pill-danger { --pill-fg: var(--fg); --pill-dot: var(--danger-fg); }

@media (forced-colors: active) {
  .pill { border: 1px solid CanvasText; }
}

.journal-actions {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 1rem;
}

.journal-tools {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
  align-items: center;
  margin: 0 0 0.75rem;
}
.journal-tools input[type="search"] { flex: 1 1 220px; }
.journal-tools label { font-size: 0.85rem; color: var(--muted); }

/* Entry rows: the whole row is the click target (client-link precedent).
   Explicit grid placement throughout — auto-placement puts the
   definite-row meta item into column 1 (the mock caught this). */
.journal-index { list-style: none; margin: 0; padding: 0; }
.journal-index li { border-bottom: 1px solid var(--line); }
.journal-index li:last-child { border-bottom: 0; }
.entry-link {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.1rem 0.9rem;
  padding: 0.75rem 0.35rem;
  text-decoration: none;
  color: inherit;
  border-radius: var(--radius-btn);
}
.entry-link:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.entry-title-row { grid-column: 1; grid-row: 1; display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.entry-title { font-weight: 600; }
.entry-excerpt {
  grid-column: 1;
  grid-row: 2;
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
}
.entry-meta {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  text-align: right;
  color: var(--muted);
  font-size: 0.85rem;
  white-space: nowrap;
}
.entry-meta .entry-words { display: block; font-size: 0.78rem; }

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin: 1rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.pagination a { font-weight: 600; }
.pagination .disabled { color: var(--muted); opacity: 0.55; }

/* Detail page. */
.entry-detail-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 0 1rem;
}
.entry-detail-meta {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0;
}
.entry-detail-meta > span + span::before { content: "·"; margin-right: 0.75rem; }
.entry-detail-actions { display: flex; gap: 0.5rem; align-items: center; }
.entry-body { max-width: 68ch; }
.entry-body h3 { margin: 1.4rem 0 0.3rem; }
.entry-body h3:first-child { margin-top: 0; }
.entry-body p { margin: 0 0 0.8rem; }

/* The writing page: a quiet, measure-limited sheet. */
.journal-compose { max-width: 76ch; }
.journal-compose-title input { width: 100%; font-size: 1.05rem; font-weight: 600; }

.prompt-picker { margin: 0.25rem 0 1rem; }
.prompt-picker-label { font-size: 0.82rem; color: var(--muted); margin: 0 0 0.4rem; }
.prompt-cards { display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: flex-start; }
.prompt-card {
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: var(--radius-btn);
  padding: 0.4rem 0.8rem;
  text-align: left;
}
.prompt-card:hover { border-color: var(--accent); background: var(--raised); }
.prompt-card small { display: block; font-weight: 500; color: var(--muted); }

/* Info dots reveal their bubble on hover or keyboard focus; a click pins
   it open (the touch path); an outside click unpins. */
.info-wrap { position: relative; display: inline-flex; }
.info-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  flex: none;
  border: 1px solid var(--muted);
  color: var(--muted);
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  cursor: help;
}
.info-dot:hover { background: none; }
.info-bubble {
  display: none;
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--ink);
  color: var(--on-ink);
  font-size: 0.78rem;
  line-height: 1.4;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-btn);
  width: max-content;
  max-width: 240px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
  z-index: 20;
}
.info-wrap:hover .info-bubble,
.info-wrap:focus-within .info-bubble,
.info-bubble.pinned { display: block; }

/* The editor: the website module's vendored Quill (snow theme),
   re-skinned only as far as the brand tokens. Before the script runs,
   the plain textarea inside posts as-is — that is the no-JS path. */
.journal-editor {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.journal-editor textarea { width: 100%; border: 0; padding: 1.1rem 1.35rem; font: inherit; }
.journal-editor .ql-toolbar.ql-snow { border: 0; border-bottom: 1px solid var(--line); font-family: inherit; }
.journal-editor .ql-container.ql-snow { border: 0; font-family: inherit; font-size: 1.02rem; }
.journal-editor .ql-editor { min-height: 46vh; padding: 1.1rem 1.35rem 1.5rem; line-height: 1.65; }
.journal-editor .ql-editor h3 {
  margin: 1.2rem 0 0.25rem;
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600;
}
.journal-editor .ql-editor h3:first-child { margin-top: 0; }
.journal-editor .ql-editor p { margin: 0 0 0.7rem; }
.journal-editor .ql-editor.ql-blank::before { color: var(--muted); font-style: normal; left: 1.35rem; right: 1.35rem; }
/* Scoped under .journal-editor: quill.snow.css loads after base.css (the
   page's scripts block), so these must win on specificity, not order. */
.journal-editor .ql-snow .ql-stroke { stroke: var(--muted); }
.journal-editor .ql-snow .ql-fill { fill: var(--muted); }
.journal-editor .ql-snow .ql-picker { color: var(--muted); }
/* Toolbar hover/active: --fg icon on --select-wash. Quill paints icons
   with stroke/fill, so an ink stroke is fine; a green stroke on light
   is not. */
.journal-editor .ql-snow.ql-toolbar button:hover,
.journal-editor .ql-snow.ql-toolbar button.ql-active,
.journal-editor .ql-snow .ql-picker-label:hover,
.journal-editor .ql-snow .ql-picker.ql-expanded .ql-picker-label,
.journal-editor .ql-snow.ql-toolbar .ql-picker-label.ql-active {
  background: var(--select-wash);
}
.journal-editor .ql-snow.ql-toolbar button:hover .ql-stroke,
.journal-editor .ql-snow.ql-toolbar button.ql-active .ql-stroke,
.journal-editor .ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke { stroke: var(--fg); }
.journal-editor .ql-snow.ql-toolbar button:hover .ql-fill,
.journal-editor .ql-snow.ql-toolbar button.ql-active .ql-fill { fill: var(--fg); }
.journal-editor .ql-snow.ql-toolbar button.ql-active,
.journal-editor .ql-snow .ql-picker-label:hover,
.journal-editor .ql-snow .ql-picker.ql-expanded .ql-picker-label,
.journal-editor .ql-snow .ql-picker-item:hover { color: var(--fg); }
.journal-editor .ql-snow.ql-toolbar .ql-picker-label.ql-active,
.journal-editor .ql-snow.ql-toolbar .ql-picker-item.ql-selected { color: var(--fg); }
.journal-editor .ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke { stroke: var(--fg); }
.journal-editor .ql-snow .ql-picker-options { background: var(--card); border-color: var(--line) !important; }
.journal-editor .ql-snow .ql-picker.ql-expanded .ql-picker-label { border-color: var(--line); }
/* The one heading level is just "Heading" to the writer, never "Heading 3". */
.journal-editor .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.journal-editor .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { content: "Heading"; }

.editor-footer {
  display: flex;
  align-items: center;
  gap: 1rem;
  border-top: 1px solid var(--line);
  padding: 0.5rem 0.75rem;
  font-size: 0.85rem;
  color: var(--muted);
}
.word-count { font-variant-numeric: tabular-nums; }
.word-count b { color: var(--fg); font-weight: 600; }
.word-count .target-met { color: var(--ok-fg); font-weight: 600; }
.editor-footer .journal-cancel { margin-left: auto; }

/* The replace guard, in the app's dialog pattern: see dialog.journal-confirm,
   dialog.record-confirm. UI refresh phase 3 (ruling Q4) moved the
   supervision session page's own "This session didn't happen" off this
   dialog onto the inline confirm bar, so dialog.record-confirm is no
   longer shared with it; only booking's requests/detail.html uses it now. */

@media (max-width: 700px) {
  .entry-meta { grid-row: auto; grid-column: 1; text-align: left; }
}

/* Journal polish (issue #94 round 2). The entry sheet: the page is
   chrome, the writing is the document. */
.entry-sheet {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1.5rem 1.75rem 1.75rem;
  max-width: 76ch;
}

/* base.css's .journal-privacy pulls itself up under the wellbeing lede
   (-0.5rem); under the compose editor that reads as an overlap. */
.journal-compose .journal-privacy { margin-top: 0.75rem; }

/* One control height across the tools row: inputs inherit the body
   line-height while selects do not, so left alone they land 4px apart.
   Flex-centred buttons keep their labels centred inside the same box. */
.journal-tools input[type="search"],
.journal-tools select,
.journal-tools .btn { height: 2.4rem; }
.journal-tools .btn { display: inline-flex; align-items: center; }

/* The CPD page's toolbar (#326): Log CPD, the year picker, and the two
   downloads pushed right, on one baseline. Below 600px each takes a full
   row and the downloads split the width evenly, so nothing is left
   ragged. One control height throughout, as the journal's tools row. */
.cpd-toolbar {
  display: grid;
  grid-template-columns: auto auto 1fr;
  grid-template-areas: "log year downloads";
  align-items: center;
  gap: 0.75rem;
  margin: 0.75rem 0 1rem;
}
.cpd-toolbar .cpd-log { grid-area: log; }
.cpd-toolbar .cpd-year-filter { grid-area: year; display: flex; align-items: center; gap: 0.5rem; margin: 0; }
.cpd-toolbar .cpd-year-filter label { font-size: 0.85rem; color: var(--muted); white-space: nowrap; }
.cpd-toolbar .cpd-year-filter select { width: auto; max-width: none; }
.cpd-toolbar .cpd-downloads { grid-area: downloads; justify-self: end; display: flex; gap: 0.6rem; }
.cpd-toolbar .btn,
.cpd-toolbar select { height: 2.4rem; }
.cpd-toolbar .btn { display: inline-flex; align-items: center; justify-content: center; }
.cpd-toolbar .btn-secondary { font-size: 0.85rem; padding: 0.3rem 0.8rem; white-space: nowrap; }
@media (max-width: 600px) {
  .cpd-toolbar {
    grid-template-columns: 1fr;
    grid-template-areas: "log" "year" "downloads";
  }
  .cpd-toolbar .cpd-year-filter select { flex: 1; min-width: 0; }
  .cpd-toolbar .cpd-downloads { justify-self: stretch; display: grid; grid-template-columns: 1fr 1fr; }
}

/* ---- Journal notebook pages (issue #130) --------------------------------
   The four newest entries as ruled pages above the list. The whole page
   is one link (the entry-link precedent). Notebook cues stop at three,
   all from the tokens: ruled lines behind the excerpt, a grey margin
   rule, a folded corner. The margin rule is grey on purpose: purple has
   its one job on light surfaces and red is a status colour (BRAND.md).
   Reference mock: .dev/journal-notebook-mockup.html, Option A. */
.journal-notebook { margin: 0 0 2rem; }
.journal-pages {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1.1rem;
}
.journal-pages.has-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.journal-pages.has-2,
.journal-pages.has-1 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.journal-page {
  --page-lh: 1.6rem; /* the ruled-line pitch */
  --page-rule: color-mix(in srgb, var(--line) 85%, transparent);
  --page-margin-rule: color-mix(in srgb, var(--muted) 38%, transparent);
  --page-fold: color-mix(in srgb, var(--card) 88%, var(--fg));
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sq);
  padding: 1rem 1.1rem 1rem 2.3rem;
  color: inherit;
  text-decoration: none;
  /* The page behind: a 3px offset copy of the surface and its border. */
  box-shadow: 3px 3px 0 -1px var(--card), 3px 3px 0 0 var(--line);
  transition: transform 0.15s ease, border-color 0.15s ease;
}
a.journal-page:hover { border-color: var(--line-strong); transform: translateY(-2px); }
/* The whole page is a link (issue #130): its own "page behind" box-shadow
   would otherwise win the property over the global :focus-visible ring
   (equal specificity, declared later), so it is restated here alongside
   the ring rather than replaced by it. */
.journal-page:focus-visible {
  outline: 2px solid transparent;
  box-shadow: 3px 3px 0 -1px var(--card), 3px 3px 0 0 var(--line), var(--focus-ring);
}
.journal-page::before { /* margin rule */
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 1.55rem;
  width: 1px;
  background: var(--page-margin-rule);
}
.journal-page::after { /* folded corner: the cut is the page background */
  content: "";
  position: absolute;
  top: -1px;
  right: -1px;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0 16px 16px 0;
  border-color: var(--bg) var(--bg) var(--page-fold) var(--page-fold);
}
.journal-page-date { /* the .fc-label register: weight and tracking, never uppercase */
  font-weight: 700;
  font-size: 0.74rem;
  letter-spacing: 0.06em;
  line-height: 1.4;
  color: var(--muted);
  margin: 0 0 0.35rem;
}
.journal-page-title {
  font-size: 1rem;
  line-height: 1.3;
  margin: 0 0 0.5rem;
  min-height: 2.6em; /* two lines reserved, so the rules align across the row */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.journal-page-excerpt {
  --page-lines: 7;
  font-size: 0.92rem;
  line-height: var(--page-lh);
  margin: 0;
  height: calc(var(--page-lines) * var(--page-lh));
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: var(--page-lines);
  -webkit-box-orient: vertical;
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(var(--page-lh) - 1px),
    var(--page-rule) calc(var(--page-lh) - 1px),
    var(--page-rule) var(--page-lh)
  );
}
.journal-page-excerpt.is-short { --page-lines: 5; }
.journal-page-foot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin: auto 0 0; /* pinned to the foot of the page */
  padding-top: 0.75rem;
  font-size: 0.78rem;
  color: var(--muted);
}
.journal-page-no {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* The blank first page: not a link, dashed, the first-entry action on it. */
.journal-page.is-empty { border-style: dashed; box-shadow: none; }
.journal-page.is-empty .journal-page-title,
.journal-page.is-empty .journal-page-excerpt { color: var(--muted); }
.journal-page.is-empty .btn { align-self: flex-start; margin: 0.9rem 0 0.25rem; }

/* Below the pages the list carries its own heading, with the tools row
   beside it, so the two halves read as one notebook. */
.journal-earlier {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 0 0 0.5rem;
}
.journal-earlier h2 { font-size: 1.1rem; margin: 0; }
.journal-earlier .journal-tools { margin: 0; flex: 1 1 420px; justify-content: flex-end; }
.journal-earlier .journal-tools input[type="search"] { flex: 0 1 260px; }
/* The base select rule is width: 100%, which in this flex row would hand
   the sort every spare pixel (the checklist form has the same override). */
.journal-earlier .journal-tools select { width: auto; }

@media (max-width: 900px) {
  .journal-pages { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .journal-pages,
  .journal-pages.has-2,
  .journal-pages.has-1 { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .journal-page { transition: none; }
  a.journal-page:hover { transform: none; }
}

/* ---- Journal export menu (issue #130) -----------------------------------
   A details disclosure styled as a secondary button, so it opens without
   JS; journal-detail.js adds Escape, outside-click and the clipboard. */
.export-menu { position: relative; }
.export-menu > summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
}
.export-menu > summary::-webkit-details-marker { display: none; }
.export-menu > summary::after { /* a small chevron */
  content: "";
  display: inline-block;
  width: 0.45em;
  height: 0.45em;
  margin-left: 0.55em;
  vertical-align: 0.15em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
}
.export-menu-list {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 10;
  min-width: min(17rem, calc(100vw - 2rem));
  margin: 0;
  padding: 0.35rem;
  list-style: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: 0 10px 30px rgba(29, 34, 56, 0.18);
}
.export-menu-list a,
.export-menu-list button {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  color: var(--fg);
  background: none;
  border: 0;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-btn);
  text-decoration: none;
  cursor: pointer;
}
.export-menu-list a:hover,
.export-menu-list button:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); }
.export-menu-label { display: block; font-weight: 600; font-size: 0.92rem; }
.export-menu-hint { display: block; font-size: 0.78rem; color: var(--muted); margin-top: 0.05rem; }
@media (max-width: 520px) {
  /* The actions row has wrapped to the left edge by now; anchor with it. */
  .export-menu-list { right: auto; left: 0; }
}

/* ---- A setting the plan holds, and a form the plan gates (#210) --------
   Starter is free because the platform fee rides on Stripe payments, so
   "ask for a card" is not optional there. The control stays visible and
   ticked with a chip naming what holds it and a link to the way out; a
   hidden field would leave the counsellor guessing why the tick will not
   move. The gate above the form dims rather than removes it, for the same
   reason: the switch itself is disabled, and the rest of the tab stays
   live so the counsellor can set booking up ready for the day they
   connect. Dimming the whole form would have said "you cannot touch
   this" about fields that are perfectly editable. */

.locked-row {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  margin: 0 0 0.85rem;
  font-size: 0.92rem;
}
.locked-row input {
  margin-top: 0.25rem;
}
.locked-row .helptext {
  display: block;
  margin-top: 0.15rem;
}

/* Paint retired for .note-warn (Task 3): the markup pairs
   class="gate-notice note-warn", and this rule only ever carries the
   icon-plus-text layout and the gap below it, which .note-warn does not
   have an opinion on.

   Task 3 review, fix 2: this is a deliberate visual change, not a
   pixel-identical swap. The old .gate-notice painted a two-step wash -
   a full 1px outer ring plus the 3px stripe, .radius-sq (2px) corners,
   0.7rem 0.85rem padding - and .note-warn is stripe-only, .radius-card
   (8px) corners, 0.6rem 0.85rem padding. Kept anyway: brief §2.7 wants
   one warn-callout look, not two, and .gate-notice was the only other
   place still painting the old two-step recipe outside the dashboard's
   milestone banners. */
.gate-notice {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  margin: 0 0 1.1rem;
}
.gate-notice p {
  margin: 0 0 0.5rem;
  font-size: 0.92rem;
}
.gate-notice p:last-of-type {
  margin-bottom: 0;
}
.gate-notice .gate-title {
  font-weight: 600;
}

/* ---- A setting derived from somewhere else (#209) ---------------------
   Online booking's hours are the counsellor's working week, so the tab
   states the week rather than offering a second grid to edit it in. Reads
   as a fact with a way out, not as a disabled form: no control chrome, a
   quiet tint off --fg so it sits below the real fields on both surfaces,
   and the link to where it IS editable in the helptext. */

.derived-row {
  border: 1px solid var(--line);
  border-radius: var(--radius-sq);
  padding: 0.7rem 0.85rem;
  margin: 0 0 1.1rem;
  background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.derived-row .derived-label {
  font-weight: 600;
  font-size: 0.92rem;
  margin: 0 0 0.3rem;
}
.derived-row dl {
  margin: 0 0 0.45rem;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.1rem 0.9rem;
  font-size: 0.9rem;
}
.derived-row dt {
  color: var(--muted);
}
.derived-row dd {
  margin: 0;
}
.derived-row .helptext {
  display: block;
}
/* The switch's help is a paragraph and a numbered list of the three ways
   to share booking (#319), so it needs a list's indent but not a page's
   paragraph spacing. */
.booking-settings-form .helptext p,
.booking-settings-form .helptext ol {
  margin: 0.25rem 0;
}
.booking-settings-form .helptext ol {
  padding-left: 1.25rem;
}

/* ---- Per-day working hours (#155) ------------------------------------ */

.week-hours {
  display: grid;
  grid-template-columns: 8.5rem 7rem auto 7rem;
  gap: 0.4rem 0.6rem;
  align-items: center;
  max-width: 28rem;
  margin-top: 0.35rem;
}
.week-row { display: contents; }
.week-row label { font-weight: 500; }
.week-row select { max-width: none; width: 100%; }
.week-to { color: var(--muted); font-size: 0.85rem; }
.week-row.is-off select, .week-row.is-off .week-to { opacity: 0.4; }

@media (max-width: 520px) {
  /* The wide layout's fixed tracks (8.5rem/7rem/auto/7rem) overflow a
     phone viewport. Take the row out of display: contents and stack the
     label above a wrapping start/to/end line instead. */
  .week-hours { grid-template-columns: none; max-width: none; }
  .week-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem 0.5rem;
    padding-bottom: 0.4rem;
  }
  .week-row label { flex: 1 1 100%; }
  .week-row select { width: auto; flex: 1 1 7rem; }
}

/* ---- Choice cards: role (#150) and plan (#157) ------------------------ */

.choice-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin: 1.25rem 0;
  padding: 0;
  border: 0;
}
@media (max-width: 640px) { .choice-grid { grid-template-columns: 1fr; } }

.choice-card {
  position: relative;
  display: block;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1.1rem 1.15rem 1.15rem 3.1rem;
  cursor: pointer;
  color: var(--fg);
}
.choice-card:hover { border-color: color-mix(in srgb, var(--accent) 60%, var(--line)); }
.choice-card input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.choice-card .fc-sq { position: absolute; left: 1.15rem; top: 1.35rem; color: var(--accent); }
.choice-card:has(input:checked) {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  background: var(--select-wash);
}
.choice-card:has(input:focus-visible) { outline: 2px solid transparent; box-shadow: var(--focus-ring); }
.choice-card:has(input:checked:focus-visible) { box-shadow: inset 0 0 0 1px var(--accent), var(--focus-ring); }
.choice-card:has(input:checked) .fc-sq { background: currentColor; }
.choice-card-heading { display: block; margin: 0 0 0.3rem; font-size: 1.05rem; }
.choice-card-blurb { display: block; margin: 0; color: var(--muted); font-size: 0.92rem; }
/* A <label> may only hold phrasing content, so the price is a <span>: it
   needs the block display the heading and blurb spans already carry. */
.choice-card .choice-price {
  display: block;
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600; font-size: 1.3rem; letter-spacing: -0.02em;
  color: var(--fg); margin: 0 0 0.35rem;
}

/* The plan step (#306) is a single choice, so it shows a round radio, not
   the brand square the role cards use. */
.choice-card .choice-radio {
  position: absolute; left: 1.15rem; top: 1.35rem;
  width: 16px; height: 16px; box-sizing: border-box;
  border: 2px solid var(--accent); border-radius: 50%;
}
.choice-card:has(input:checked) .choice-radio {
  background: radial-gradient(circle, var(--accent) 0 3.5px, transparent 4px);
}
.fee-example {
  display: block; margin: 0.5rem 0 0.6rem; padding: 0.5rem 0.7rem;
  border: 1px solid var(--line); border-radius: var(--radius-card);
  font-size: 0.92rem;
}
.fee-row { display: flex; justify-content: space-between; gap: 1rem; padding: 0.15rem 0; }
.fee-total { border-top: 1px solid var(--line); margin-top: 0.25rem; padding-top: 0.35rem; font-weight: 600; }
/* The session-fee calculator under Session fee (#307). */
.fee-calculator { margin: -0.35rem 0 1rem; max-width: 34rem; }
.fee-calculator .fee-example { max-width: 26rem; }
.fee-calculator-ask, .fee-calculator-answer {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0 0 0.4rem;
}
.fee-calculator-ask label { margin: 0; font: inherit; }
.fee-calculator-ask input { width: 6.5rem; }
.choice-card .choice-features { display: block; margin: 0.35rem 0 0.75rem; }
.choice-card .choice-feature { display: block; padding-left: 1.1rem; position: relative; font-size: 0.95rem; }
.choice-card .choice-feature::before { content: "\2713"; position: absolute; left: 0; color: var(--accent); }
/* Starter's worked example (#662): set apart so it reads as an example,
   not as the plan's price. */
.choice-card .choice-example {
  display: block; margin: 0.5rem 0 0.6rem; padding: 0.5rem 0.7rem;
  border: 1px solid var(--line); border-radius: var(--radius-card);
  font-size: 0.92rem;
}
.choice-card .choice-example-label { display: block; font-style: italic; margin: 0 0 0.2rem; }
.choice-card .choice-example-line { display: block; padding: 0.1rem 0; }

/* ---- Account setup card with a radial gauge (#152) ------------------- */

/* ---- Switch (issue #227) ------------------------------------------------
   A two-state control that posts, shared by templates/forms/_switch.html.
   Square, not a pill: the 999px radius is reserved for the named list in
   PILL_RADIUS_SELECTORS (the status pill and the calendar zoom toggles)
   and James asked for this to stay inside the radius scale instead. Do
   not write the pill radius here, not even in a comment, because
   test_only_the_named_components_are_pills counts it as a raw substring
   of the whole sheet. The on state is a filled control, so it takes the
   accent pair: ink with a green label on light, green with an ink label
   on dark. The thumb is --on-accent, which
   test_on_accent_is_legible_against_the_accent_fill already pins at
   4.5:1 or better against that fill. */

.switch {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0;
  font: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

.switch:hover { background: none; }

.switch-track {
  position: relative;
  flex: none;
  width: 2.25rem;
  height: 1.25rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-btn);
  transition: background-color 120ms ease, border-color 120ms ease;
}

.switch-thumb {
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 0.875rem;
  height: 0.875rem;
  background: var(--muted);
  border-radius: var(--radius-sq);
  transition: transform 120ms ease, background-color 120ms ease;
}

.switch[aria-checked="true"] .switch-track {
  background: var(--accent);
  border-color: var(--accent);
}

.switch[aria-checked="true"] .switch-thumb {
  background: var(--on-accent);
  transform: translateX(1rem);
}

.switch[disabled] { opacity: 0.4; cursor: default; }

.switch-form { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .switch-track, .switch-thumb { transition: none; }
}

/* The one .widget outside .dashboard-grid, so it brings its own margin
   instead of a grid gap. The bottom half matters: .widget-attention has no
   top margin, so without it the two cards collapse to a zero gap (#227). */
.widget-setup { margin: 1rem 0 1.25rem; }
.setup-body { display: grid; grid-template-columns: 8rem 1fr; gap: 0.5rem 1.5rem; align-items: start; margin-top: 0.5rem; }
@media (max-width: 720px) { .setup-body { grid-template-columns: 1fr; } }
.setup-gauge { text-align: center; padding-top: 0.25rem; }
.gauge { width: 7rem; height: 7rem; display: block; margin: 0 auto; }
.gauge-track { fill: none; stroke: var(--line); stroke-width: 8; }
/* The fill is the completion colour (--ok-fg, BRAND.md), not the accent. */
.gauge-fill { fill: none; stroke: var(--ok-fg); stroke-width: 8; }
.gauge-value {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600; font-size: 24px; letter-spacing: -0.02em; fill: var(--fg);
}
.setup-count { font-size: 0.85rem; color: var(--muted); margin: 0.35rem 0 0; white-space: nowrap; }
.setup-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 0 2rem; }
@media (max-width: 900px) { .setup-columns { grid-template-columns: 1fr; } }
.setup-steps { list-style: none; padding: 0; margin: 0; }
/* A minimum, not a fixed height: at 375px a long label wraps beside its
   buttons, and a fixed row let it run into the next (app#677). */
.setup-steps li {
  display: flex; align-items: center; gap: 0.6rem;
  min-height: 2.9rem; padding: 0.3rem 0; box-sizing: border-box;
  border-top: 1px solid var(--line); font-size: 0.92rem;
}
/* Green marks completion only (BRAND.md "The square"): scoped to done rows
   so the open squares of outstanding steps don't read as already handled.
   Open squares are left on the row's inherited text colour rather than
   --muted, matching checklist.html's open/pending square, which carries no
   colour override either — and it keeps the mark as legible as the bold
   label beside it. */
.setup-steps .is-done .fc-sq { color: var(--ok-fg); }
.setup-steps .is-done .setup-label { color: var(--muted); }
.setup-steps .is-todo .setup-label { font-weight: 600; }
.setup-actions { margin-left: auto; display: flex; gap: 0.4rem; flex: none; }
.setup-actions .btn { padding: 0.28rem 0.75rem; font-size: 0.82rem; }

/* ---- Stripe cards (#168, Plan B) --------------------------------------- */

/* One card per Stripe object, on the wizard's Stripe steps and the
   Settings Payments tab. The status line is text on the plain card
   surface, never a wash: red (--warn-fg) says "needs action", the same
   pairing the warn pill's text colour uses; --ok-fg is completion only
   (BRAND.md). */
.stripe-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1.1rem 1.25rem;
  margin: 1.25rem 0;
}
.stripe-card-heading { margin: 0 0 0.35rem; font-size: 1.05rem; }
.stripe-card-blurb { margin: 0 0 0.75rem; color: var(--muted); font-size: 0.92rem; }
.stripe-card form { display: inline-block; margin: 0.25rem 0.5rem 0.75rem 0; }
.stripe-status { margin: 0 0 0.5rem; font-weight: 600; }
.stripe-status-warn { color: var(--warn-fg); }
.stripe-status-done { color: var(--ok-fg); }
.wizard-stripe-actions { display: flex; align-items: center; gap: 0.25rem; }

/* Skip is a state change, so it is a POST form beside the link button.
   .setup-actions is a flex line, which blockifies the form anyway; what
   the form must not bring to it is a margin of its own. */
.setup-actions form { margin: 0; }

/* ---- Safeguarding (#128) ----------------------------------------------- */

/* The responsibility statement is templates/core/safeguarding.html's own
   <p class="note">: a neutral aside, not a status (Task 3) — what it says
   is true of everything below it, not a fact about the page's state, so
   it takes .note's plain ink-on-tint rather than .note-info. It leads the
   page rather than sitting inside a section for the same reason. */

/* Escalation contacts are a table: who they are, then phone and email in
   columns of their own so the numbers line up down the page, then Edit.
   They used to be record rows, where every link took the group's
   `margin-left: auto` and wrapped wherever it landed. Below 600px each
   row restacks into a card: who, then phone and email, then Edit. */
.escalation-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 0.75rem;
  font-size: 0.92rem;
}
.escalation-table thead th {
  text-align: left;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
  padding: 0 0.9rem 0.35rem 0.1rem;
  border-bottom: 1px solid var(--line-strong);
}
.escalation-table tbody th,
.escalation-table tbody td {
  text-align: left;
  vertical-align: baseline;
  padding: 0.6rem 0.9rem 0.6rem 0.1rem;
  border-bottom: 1px solid var(--line);
}
.escalation-table tbody tr:last-child > * { border-bottom: none; }
.escalation-who { font-weight: normal; }
.escalation-who > span { display: block; }
.escalation-name { font-weight: 600; }
.escalation-meta { color: var(--muted); font-size: 0.85rem; }
.escalation-note { color: var(--fg-soft); font-size: 0.85rem; margin-top: 0.15rem; }
.escalation-phone { white-space: nowrap; font-variant-numeric: tabular-nums; }
.escalation-email { overflow-wrap: anywhere; }
.escalation-table .escalation-edit { text-align: right; padding-right: 0.1rem; font-size: 0.85rem; }
@media (max-width: 600px) {
  .escalation-table thead { display: none; }
  .escalation-table tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.25rem 0.9rem;
    padding: 0.6rem 0.1rem;
    border-bottom: 1px solid var(--line);
  }
  .escalation-table tbody tr:last-child { border-bottom: none; }
  .escalation-table tbody th,
  .escalation-table tbody td { display: block; padding: 0; border: none; }
  .escalation-who { grid-column: 1; grid-row: 1; }
  .escalation-table .escalation-edit { grid-column: 2; grid-row: 1; }
  .escalation-phone, .escalation-email { grid-column: 1 / -1; }
  .escalation-phone:empty, .escalation-email:empty { display: none; }
}

.helpline-group {
  margin: 1.1rem 0 0;
  font-size: 1rem;
}
.helpline-group:first-of-type { margin-top: 0.25rem; }

/* ---- Toast (brief §2.9, Task 5) -----------------------------------------
   Ink with a green tick in light; the dark track (D2) flips it to neon
   with ink. One at a time (static/js/toast.js replaces rather than
   stacks), in a polite live region present on every page since P3 moves
   `success` messages here instead of the inline .messages list. */
.toast-region {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  display: flex;
  justify-content: center;
  transform: none;
  z-index: 60;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.6rem 0.6rem 0.9rem;
  border-radius: var(--radius-card);
  background: var(--ink);
  /* #ffffff is a known exception until D1 adds --toast-fg: the toast sits
     on --ink in both themes until D2, so there is no dark-theme flip to
     resolve against yet. */
  color: #ffffff;
  border: 1px solid var(--shell-line);
  box-shadow: 0 12px 30px -10px rgb(29 34 56 / 0.5);
  font-size: 0.9rem;
  max-width: 40rem;
  pointer-events: auto;
  animation: toast-in 0.2s ease-out;
  /* The toast's own controls sit on ink regardless of theme (like the
     chrome header and app shell), so the ring restates green-on-ink. */
  --ring-gap: var(--ink);
  --ring-inner: var(--green);
  --ring-outer: transparent;
}

.toast.toast-no-anim { animation: none; }

/* A CSS-drawn tick: no check/circle-check icon exists yet in
   templates/icons/ (brief §2.9). Raw --green as an icon colour is safe
   here specifically because the toast is always the ink surface, never
   light — see the constraint this component would otherwise break. */
.toast-tick { width: 1rem; height: 1rem; color: var(--green); flex: none; position: relative; }
.toast-tick::after {
  content: "";
  position: absolute;
  left: 0.28rem;
  top: 0.02rem;
  width: 0.32rem;
  height: 0.62rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}

.toast-text { flex: 1 1 auto; }

.toast-undo {
  background: transparent;
  color: var(--green);
  border: 1px solid var(--shell-line);
  padding: 0.35rem 0.7rem;
}
.toast-undo:hover { background: transparent; border-color: var(--green); }
.toast .toast-close { color: var(--on-ink); }
.toast .toast-close:hover { background: var(--ink-raised); color: #ffffff; }

@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }

/* Adding a second role (#304): the Courses page boxes, the dashboard
   prompt, the Course Progress footnote and the wizard's "Not now". */
.role-change { max-width: 44rem; }
.role-change p { color: var(--muted); }
.role-change form { margin: 0; }
.dashboard-prompt { margin: 1.25rem 0 0; color: var(--muted); font-size: 0.95rem; }
.widget-footnote { margin: 0.75rem 0 0; font-size: 0.9rem; color: var(--muted); }
.wizard-not-now { margin: 1rem 0 0; }

/* ---- Courses page ------------------------------------------------------
   A course card, then every target with its live position in two groups:
   hours to reach (bars) and rules to keep to (a state, never a bar, #64).
   Mock: .dev/courses-page-mock. */

.course-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 1.25rem 1.4rem;
  margin: 1rem 0 1.5rem;
}
.course-card-head { display: flex; align-items: flex-start; gap: 1rem; }
/* The sidebar's pairing, green on ink. The inset line keeps the tile's
   edge on the dark theme's ink-raised card. */
.course-card-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-card);
  background: var(--ink);
  color: var(--green);
  box-shadow: inset 0 0 0 1px var(--line);
}
.course-card-mark svg { width: 22px; height: 22px; }
.course-card-title { flex: 1; min-width: 0; }
.course-card-title h2 { margin: 0; font-size: 1.3rem; line-height: 1.25; }
.course-card-title p { margin: 0.2rem 0 0; color: var(--muted); }
.course-card-head .btn-secondary { flex: none; }
.course-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem 1.5rem;
  margin: 1.1rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}
.course-facts dt { font-size: 0.78rem; color: var(--muted); margin: 0 0 0.15rem; }
.course-facts dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
.course-facts .course-fact-sub { display: block; font-weight: 400; font-size: 0.9rem; }
.course-facts dd.is-missing { font-weight: 400; color: var(--muted); }

.course-targets, .course-activity { margin: 0 0 1.5rem; }
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.35rem;
}
.section-head h2 { margin: 0; }
.section-lede { color: var(--muted); margin: 0 0 1rem; max-width: 62ch; }
.targets-hint { color: var(--muted); font-size: 0.9rem; margin: 0.25rem 0 0; }

.target-group {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  margin: 0 0 1rem;
  overflow: hidden;
}
.target-group > h3 {
  margin: 0;
  padding: 0.7rem 1.2rem;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
}
.target-list { list-style: none; margin: 0; padding: 0; }
/* Every column but the meter is fixed, so every meter is the same width
   and the per-row today lines join into one rule down the list. */
.target-row {
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr) 7.5rem 3rem;
  grid-template-areas: "name meter figure edit";
  align-items: stretch;
  column-gap: 1.25rem;
  padding: 0 1.2rem;
  border-bottom: 1px solid var(--line);
}
.target-row:last-child { border-bottom: 0; }
.target-row > * { padding: 0.85rem 0; }
.target-name { grid-area: name; display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.target-name strong { font-weight: 600; }
.target-meta { color: var(--muted); font-size: 0.85rem; }
.target-meter { grid-area: meter; position: relative; display: flex; align-items: center; gap: 0.6rem; }
.target-meter .progress { flex: 1; height: 8px; border-radius: 4px; margin: 0; }
.target-meter .progress-fill { border-radius: 4px; }
.rule-note { color: var(--muted); font-size: 0.85rem; }
.target-figure {
  grid-area: figure;
  align-self: center;
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.target-figure b {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600;
  font-size: 1.05rem;
}
.target-figure span { color: var(--muted); font-size: 0.85rem; }
.target-edit { grid-area: edit; justify-self: end; align-self: center; font-size: 0.85rem; }

/* The today line: the full row height, so adjacent rows join up; inset
   at the list's two ends so it does not run into the group's edges. */
.pace-line {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pace);
  width: 0;
  border-left: 2px dashed var(--fg);
  opacity: 0.55;
}
.target-list .target-row:first-child .pace-line { top: 0.85rem; }
.target-list .target-row:last-child .pace-line { bottom: 0.85rem; }
/* Course time is ink, not green: time passing is not progress. */
.target-row.is-time { background: var(--raised); }
.target-row.is-time .progress-fill { background: var(--fg); opacity: 0.8; }
.target-list .target-row.is-time .pace-line { top: 1.45rem; opacity: 0.9; }
.pace-flag {
  position: absolute;
  top: 0.2rem;
  left: var(--pace);
  transform: translateX(-50%);
  padding: 0 0.3rem;
  font-size: 0.7rem;
  font-weight: 700;
  white-space: nowrap;
  background: var(--raised);
}

.activity-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: 0.35rem 1.2rem;
  margin-top: 1rem;
}
.activity-list { list-style: none; margin: 0; padding: 0; }
.activity-list li {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr) 10rem 4.5rem 3rem;
  gap: 0.2rem 1rem;
  align-items: baseline;
  padding: 0.65rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}
.activity-list li:last-child { border-bottom: 0; }
.activity-more > .activity-list { border-top: 1px solid var(--line); }
.activity-date, .activity-target { color: var(--muted); }
.activity-title { font-weight: 600; overflow-wrap: anywhere; }
/* A record named by its title on a delete confirm (app#526): long titles wrap. */
.confirm-name { overflow-wrap: anywhere; }
.activity-mins { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.activity-list li > a { justify-self: end; font-size: 0.85rem; }
.activity-more { margin: 0.25rem 0 0.4rem; font-size: 0.9rem; }
.activity-more > summary { display: inline-block; cursor: pointer; margin: 0.35rem 0; }
.activity-more[open] > summary { display: none; }
.activity-empty { color: var(--muted); }

.role-change-band {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  margin-top: 2rem;
  padding: 1.25rem 0 0;
  border-top: 1px solid var(--line);
}
.role-change-band h2 { font-size: 1rem; margin: 0 0 0.2rem; }
.role-change-band p { margin: 0; color: var(--muted); max-width: 60ch; font-size: 0.92rem; }
.role-change-band form { margin: 0; flex: none; }

.target-remove { margin: 1.5rem 0 0; }

@media (max-width: 760px) {
  .course-facts { grid-template-columns: 1fr; }
  .course-card-head { flex-wrap: wrap; }
  .course-card-title { flex-basis: calc(100% - 3.75rem); }
  .course-card-title h2 { font-size: 1.15rem; }
  .course-card-head .btn-secondary { order: 3; }
  .section-head { flex-wrap: wrap; }
  .target-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "name figure" "meter meter" "edit edit";
    padding: 0.75rem 1rem;
  }
  .target-row > * { padding: 0; }
  .target-meter { min-height: 1.4rem; margin-top: 0.5rem; }
  .target-edit { justify-self: start; margin-top: 0.35rem; }
  /* Stacked rows no longer share a line, so each keeps a short tick. */
  .target-list .target-row:nth-child(n) .pace-line { top: 0.15rem; bottom: 0.15rem; border-left-style: solid; }
  .pace-flag { display: none; }
  .activity-list li {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "title title mins" "date target edit";
  }
  .activity-title { grid-area: title; }
  .activity-mins { grid-area: mins; }
  .activity-date { grid-area: date; font-size: 0.85rem; }
  .activity-target { grid-area: target; font-size: 0.85rem; }
  .activity-list li > a { grid-area: edit; }
  .role-change-band { flex-direction: column; align-items: flex-start; }
}

/* ---- Approvals (D64, app#268) -------------------------------------------
   The student's "What would you like to submit?" cards and a card's
   item list with its summary drawer. Everything rides the base tokens. */
.appr-notice { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; align-items: baseline; }
.appr-grid {
  list-style: none; padding: 0; margin: 0 0 1.25rem;
  display: grid; grid-template-columns: 1fr; gap: 0.75rem;
}
@media (min-width: 700px) { .appr-grid { grid-template-columns: 1fr 1fr; } }
.appr-card {
  display: flex; flex-direction: column; gap: 0.3rem; height: 100%;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-card);
  padding: 1rem 1.1rem; color: var(--fg); text-decoration: none;
}
.appr-card:hover { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.appr-count {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-size: 1.6rem; font-weight: 600; letter-spacing: -0.02em; line-height: 1.15; margin-top: 0.2rem;
}
.appr-count-done { font-size: 1.05rem; color: var(--ok-fg); margin: 0.4rem 0 0.35rem; }
.appr-card.is-done { background: color-mix(in srgb, var(--green) 8%, var(--card)); }
.appr-sub { color: var(--muted); font-size: 0.88rem; }
.appr-hours {
  margin-top: auto; padding-top: 0.55rem; border-top: 1px solid var(--line);
  font-size: 0.85rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.appr-tabs { margin: 0 0 1rem; }
.appr-selectall { margin: 0 0 0.6rem; font-size: 0.92rem; }
.appr-items { list-style: none; padding: 0; margin: 0 0 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.appr-item {
  display: flex; gap: 0.75rem; align-items: flex-start;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius-card);
  padding: 0.7rem 0.9rem; font-size: 0.92rem;
}
label.appr-item { cursor: pointer; }
.appr-item:has(input:checked) { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); background: var(--select-wash); }
.appr-item input { margin-top: 0.1rem; flex: none; }
.appr-item-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.appr-item-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.9rem; }
.appr-item-line > span:not(.pill) { color: var(--muted); }
.appr-needs { color: var(--muted); font-size: 0.82rem; }
.appr-comment { display: block; font-size: 0.85rem; margin-top: 0.2rem; }
.appr-drawer {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; align-items: center;
  margin: 1.5rem 0 0; padding: 1rem 1.4rem 1.1rem;
  background: var(--card); border: 1px solid var(--line-strong); border-bottom: 0;
  border-radius: var(--radius-panel) var(--radius-panel) 0 0;
  box-shadow: 0 -8px 24px rgba(29, 34, 56, 0.12);
}
.appr-drawer p { margin: 0; }
.appr-drawer-summary { flex: 1 1 22rem; display: flex; flex-direction: column; gap: 0.3rem; }
.appr-drawer-total { font-size: 1.05rem; font-weight: 600; }
.appr-drawer-to { font-size: 0.9rem; }
.appr-drawer-micro { color: var(--muted); font-size: 0.85rem; }
.appr-pick { font-size: 0.9rem; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* "Add your placement manager's email to send this request" (app#689). */
.appr-manager { border: 0; padding: 0; margin: 0.5rem 0; display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.9rem; max-width: 26rem; }
.appr-manager legend { padding: 0; font-weight: 600; }
.appr-manager-for { margin: 0; color: var(--muted); }
.appr-manager > .errorlist { margin: 0; }
.appr-drawer-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem; }
.appr-paper { font-size: 0.85rem; max-width: 22rem; }
.appr-paper summary { cursor: pointer; }
.appr-paper label { display: flex; flex-direction: column; gap: 0.25rem; margin: 0.5rem 0; }
.appr-sent { text-align: center; padding: 2.25rem 1.5rem; }
.appr-sent h2 { font-size: 1.25rem; margin: 0 0 0.4rem; }
.appr-sent p { color: var(--muted); max-width: 44ch; margin: 0 auto 1.1rem; }
@media (max-width: 600px) {
  .appr-drawer { padding: 0.9rem 1rem; }
  .appr-drawer-actions { align-items: stretch; width: 100%; text-align: center; }
}
.appr-locked-badge { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.8rem; margin: 0 0 0.9rem; }
.appr-lock-note { color: var(--muted); font-size: 0.9rem; }
.appr-scan-note { color: var(--danger-fg); font-weight: 600; margin: 0.4rem 0 0; }

/* ==== app#600 PR A: controls and buttons ================================
   Appended as one block (base.css is shared with another task's work at
   the time of writing): browser-default controls that sat among the
   branded ones, and buttons that came in three sizes for the same kind of
   action. Element-level rules only fill in what nothing else sets, so the
   classed fieldsets and legends above keep their own treatment. */

/* Radios in the palette, as checkboxes already are. */
input[type="radio"] { accent-color: var(--accent); }

/* One fieldset frame: the palette's line and card radius in place of the
   browser's grey groove ("Apply changes to", "Cancellation policy"). */
fieldset {
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  min-width: 0;
}
legend {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-weight: 600;
}

/* File pickers: the browser's "Choose File" gets the secondary button. */
input[type="file"] { font: inherit; font-size: 0.92rem; max-width: 100%; }
input[type="file"]::file-selector-button {
  font-family: 'Montserrat Variable', ui-sans-serif, system-ui, sans-serif;
  font-size: 0.92rem;
  font-weight: 600;
  padding: 0.45rem 1rem;
  margin-right: 0.75rem;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-btn);
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--raised); }

/* One button size. A <button> is 0.92rem; an <a class="btn"> took the
   page's 1rem, so link-buttons ("Edit membership and qualifications",
   "Log CPD", "Change password") stood larger than the Save beside them.
   .btn-compact (above) did this for one row; now every .btn matches. */
.btn, .quick-links a { font-size: 0.92rem; }

/* CPD's Log CPD keeps its button width on a phone instead of a full-width
   bar; the year picker and downloads still take their own rows. */
@media (max-width: 600px) {
  .cpd-toolbar .cpd-log { justify-self: start; }
}

/* Settings: the shared Save sits inside the card, as the Payments,
   Online booking and Professional body tabs' own Saves do. Only once the
   tabs are live (the toolbar is revealed by settings-tabs.js): without
   JavaScript every section shows, stacked, with the one Save below. */
.tabs:not([hidden]) ~ .settings-form > .dashboard-section {
  border-bottom: 0;
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  margin-bottom: 0;
  padding-bottom: 0.25rem;
}
.tabs:not([hidden]) ~ .settings-form > .settings-actions {
  background: var(--card);
  border: 1px solid var(--line);
  border-top: 0;
  border-radius: 0 0 var(--radius-card) var(--radius-card);
  margin: 0 0 1rem;
  padding: 1rem 1.4rem 1.25rem;
}

/* Settings > Your practice: the website question is a heading over its
   cards, not a legend cut into a frame (Settings boxes every other
   fieldset; element plus class out-ranks .settings-form fieldset). */
.settings-form fieldset.choice-grid { border: 0; padding: 0; border-radius: 0; }
.settings-form fieldset.choice-grid > legend {
  padding: 0;
  margin-bottom: 0.6rem;
  font-size: 0.95rem;
}

/* Settings > Payments: the fee calculator's ask on a phone. The question
   reads as one sentence, its amount box inline, and Work it out sits on
   its own line below rather than trailing a ragged third line. */
@media (max-width: 480px) {
  .fee-calculator-ask { display: block; }
  .fee-calculator-ask label { display: inline; }
  .fee-calculator-ask input { width: 5.5rem; margin: 0 0.3rem; vertical-align: middle; }
  .fee-calculator-ask > button { display: block; margin-top: 0.5rem; }
}

/* ==== app#600 PR C: phone layouts and lists ============================
   Appended as one block, like PR A's. */

/* Client record: Upcoming and Past sit under small headings in the
   Sessions card, the older past behind a native Show more (#415). */
.session-list-heading {
  font-size: 0.95rem;
  margin: 1rem 0 0.15rem;
}
details.session-more { margin-top: 0.25rem; }
details.session-more > summary { cursor: pointer; font-weight: 600; padding: 0.35rem 0; }

/* On a phone a session row is two lines: the date with Edit at its end,
   then the length, type and any chips. Without this the row wrapped
   wherever it ran out, leaving Edit on line one or two at random. The
   ::after is a zero-height full-width flex item that forces the break. */
@media (max-width: 480px) {
  .session-list > li.session { row-gap: 0.1rem; }
  .session-list > li.session > .session-date { order: 0; }
  .session-list > li.session > a { order: 1; margin-left: auto; }
  .session-list > li.session::after { content: ""; order: 2; flex-basis: 100%; height: 0; }
  .session-list > li.session > span:not(.session-date) { order: 3; }
}

/* Journal: the three-page notebook stacked too late. .has-3 out-ranked
   the media queries' bare .journal-pages, so a phone kept three columns of
   one word per line. */
@media (max-width: 900px) {
  .journal-notebook .journal-pages { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .journal-notebook .journal-pages { grid-template-columns: 1fr; }
}

/* Safeguarding: a phone number never breaks across lines. */
.record-facts a[href^="tel:"] { white-space: nowrap; }

/* ---- Help button and first sign-in tour (app#675) ----------------------
   The Help button sits at the top right of every app-shell page, so the
   page header leaves it room. The tour is a full-screen transparent modal
   dialog: .tour-spot outlines the target and dims everything else with one
   huge box-shadow, so the page behind stays visible. */
.app-shell-main { position: relative; }
.app-shell-main > .fc-page-header { padding-right: 7rem; }

.help-menu { position: absolute; top: 0; right: 1.5rem; z-index: 20; }
.help-menu-toggle { display: inline-flex; align-items: center; gap: 0.4rem; }
.help-menu-toggle svg { width: 1.05rem; height: 1.05rem; flex: none; }
.help-menu-toggle svg:last-child { width: 0.9rem; height: 0.9rem; }
.help-menu-toggle[aria-expanded="true"] svg:last-child { transform: rotate(180deg); }
.help-menu-panel {
  position: absolute; right: 0; top: calc(100% + 0.35rem);
  min-width: 15rem; margin: 0; padding: 0.35rem; list-style: none;
  background: var(--card); color: var(--fg);
  border: 1px solid var(--line); border-radius: var(--radius-card);
  box-shadow: 0 8px 24px rgba(29, 34, 56, 0.18);
}
.help-menu-panel a,
.help-menu-panel button {
  display: block; width: 100%; text-align: left;
  padding: 0.55rem 0.75rem; border-radius: var(--radius-btn);
  background: none; border: 0; color: var(--fg); font: inherit; font-weight: 500;
  text-decoration: none; cursor: pointer;
}
.help-menu-panel a:hover,
.help-menu-panel button:hover { background: var(--raised); }

dialog.tour {
  position: fixed; inset: 0; width: 100vw; height: 100vh; max-width: none; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; overflow: hidden;
}
dialog.tour::backdrop { background: transparent; }
.tour-spot {
  position: absolute; border-radius: var(--radius-card);
  outline: 3px solid var(--green);
  box-shadow: 0 0 0 100vmax rgba(29, 34, 56, 0.5);
  pointer-events: none;
}
.tour-dim { position: absolute; inset: 0; background: rgba(29, 34, 56, 0.5); }
.tour-card {
  position: absolute; width: min(22rem, calc(100vw - 2rem));
  background: var(--card); color: var(--fg);
  border: 1px solid var(--line); border-radius: var(--radius-panel);
  padding: 1.1rem 1.25rem 1rem; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
}
.tour-card.is-centred { left: 50%; top: 50%; transform: translate(-50%, -50%); }
.tour-count { margin: 0 0 0.2rem; color: var(--muted); font-size: 0.85rem; }
.tour-card h2 { margin: 0 0 0.35rem; font-size: 1.1rem; }
.tour-card .tour-text { margin: 0 0 1rem; line-height: 1.5; }
.tour-actions { display: flex; align-items: center; gap: 0.5rem; }
.tour-actions .tour-skip {
  margin-right: auto; padding: 0.35rem 0;
  background: none; border: 0; color: var(--fg); font-weight: 500;
  text-decoration: underline; cursor: pointer;
}

.help-stub { max-width: 40rem; }
.help-stub p { line-height: 1.6; }
.help-stub .settings-actions { margin-top: 1.25rem; }

@media (max-width: 900px) {
  .help-menu { right: 1rem; }
  .app-shell-main > .fc-page-header { padding-right: 5.5rem; }
  .tour-card.is-docked { left: 1rem; right: 1rem; bottom: 1rem; width: auto; }
}
