/* The whole visual design of the forum app, in one file with no build step.
 *
 * WHAT THIS IS TRYING TO LOOK LIKE
 * Good stationery. Six senior people open this on a laptop in a hotel and on a
 * phone at breakfast, to write down the things they tell nobody else. A tool
 * for that should be quiet: high contrast, generous whitespace, one accent, one
 * hairline, one radius, and no decoration that has not earned its place. There
 * are no gradients, no drop shadows on everything, no coloured cards competing
 * for attention. The page should feel like the paper form it replaces - which
 * is also the form these members already know by eye.
 *
 * COLOUR EXISTS ONLY AS TOKENS
 * Every colour in this file is declared once in the :root block and once in the
 * dark override, and nothing below those two blocks contains a raw hex. That is
 * not tidiness: it is the only way a second theme stays correct. The day
 * somebody hard-codes #fff on a button, that button is white text on white in
 * dark mode and nobody notices until a member reads it at midnight.
 *
 * THE EMOTION WHEEL IS PART OF THE PALETTE, NOT AN ILLUSTRATION
 * The seven core colours are the ones on the forum's own printed wheel and are
 * mirrored from server/wheel.js CORE_COLOURS. They are pastels, chosen for ink
 * on paper: in dark mode the HUE is kept and the lightness is dropped, so a
 * chip still reads as "that's the yellow one" while the label on it clears
 * 4.5:1. Every pair in this file was measured, not eyeballed; the dark set sits
 * between 5.0:1 and 6.8:1 against --core-ink.
 *
 * THE PHONE IS NOT AN AFTERTHOUGHT
 * The 5% form is a five-column table on a laptop because that is the shape of
 * the Word form. On a phone that table cannot be read, so below 760px it stops
 * being a table and becomes one card per entry, each cell labelled by CSS. The
 * page body never scrolls sideways; anything genuinely wide scrolls inside its
 * own .tablewrap.
 *
 * SPACING IS A SCALE, AND THE SCALE IS FOUR STEPS
 * Every BOX INSET in this file - what a card, a row, a cell, a panel or a
 * group puts between its edge and its contents - is --pad-1 through --pad-4 and
 * nothing else. It was not always: an audit found 13px beside 15px beside 16px
 * on three boxes that sit in the same row of the budget page, and a list row
 * inside a card body inset twice, so its text started 32px in while the
 * paragraph above it started at 16. Neither is a bug anyone reports; together
 * they are why a page looks slightly wrong and nobody can say where.
 *
 * CONTROL padding is NOT on that scale and must not be forced onto it. A
 * button's 7px 14px and an input's 9px 11px are optical sizes - they are what
 * makes a button the height of the input beside it - and rounding them to the
 * nearest box step changes the size of the control rather than its inset.
 *
 * Anything larger than --pad-4 is a deliberate step outside the scale (the page
 * gutter, a sign-in card) and is written as a sum of steps so it is still
 * obviously ON the ladder rather than a number somebody typed.
 */

:root {
  /* Paper and ink. --bg is warm off-white rather than blue-grey: the blue-grey
     dashboard look is exactly what this app is not. */
  --bg: #f7f6f3;
  --surface: #ffffff;
  --surface-sunken: #f2f1ec;
  --line: #ddd9d0;
  --line-soft: #ebe8e1;
  --ink: #1b2129;
  --ink-muted: #5c6672;
  /* THE THIRD TIER IS TEXT, SO IT HAS TO BE READABLE. This was #8b93a0, which
     measures 3.1:1 on --surface and 2.7:1 on the sunken ground a hovered row
     paints underneath it - and .faint is not decoration, it is the class the
     markup puts on a member's converted balance, on the meetings a parked item
     was raised at, and on the sentence explaining why a control is missing.
     Now 5.2:1 on --surface, 4.8:1 on --bg, 4.6:1 on --surface-sunken and
     4.6:1 on --accent-soft: legible on every ground in this file.
     The cost is a narrower ladder - muted is 5.8:1, faint 5.2:1 - so the third
     tier is now separated from the second by SIZE and WEIGHT (it is almost
     always worn with .small) rather than by a lightness step that could only
     be that big by being illegible. That is the right way round. */
  --ink-faint: #646d7b;

  /* ONE accent, used for action and for "you are here". Nothing else is blue. */
  --accent: #1f5fa8;
  --accent-ink: #ffffff;
  --accent-soft: #eaf1f9;
  --accent-line: #b9d2ee;

  /* Semantic colours. Deliberately few - a status vocabulary of four is one a
     reader can hold in their head. */
  --ok: #186a3f;
  --ok-soft: #e8f3ec;
  --warn: #8a5a10;
  --warn-soft: #f8f0e2;
  --danger: #a3341f;
  --danger-soft: #faeeeb;
  --danger-line: #e8c4ba;

  /* The confidentiality mark. It is a second colour and it breaks the one-accent
     rule on purpose: --accent means "you may act", --seal means "only you can
     see this". Those two must never be confused, and a member glancing at a
     screen in a lobby needs the answer in one look, not in a sentence. */
  --seal: #6b4b8a;
  --seal-soft: #f2eef7;
  --seal-line: #d8cbe8;

  /* The emotion wheel, from server/wheel.js CORE_COLOURS. */
  --core-happy: #f3e07f;
  --core-surprised: #c9b6e4;
  --core-bad: #9fd6b4;
  --core-fearful: #f5d089;
  --core-angry: #f2a6a0;
  --core-disgusted: #b9b9b9;
  --core-sad: #a8c6e8;
  /* One ink for all seven swatches: a per-swatch text colour would be seven
     more things to keep true across two themes. Measured 8.4:1 (Angry, the
     worst case) to 12.4:1 (Happy) in light. */
  --core-ink: #16202c;
  --core-edge: rgba(27, 33, 41, .18);

  /* Focus and overlay. Both are colour, so both are tokens. */
  --focus: #1f5fa8;
  --scrim: rgba(27, 33, 41, .38);
  /* One shadow, barely there, used only where something genuinely floats. */
  --lift: 0 6px 24px rgba(27, 33, 41, .12);

  --radius: 10px;
  --radius-sm: 7px;

  /* THE SPACING SCALE. Four steps, 4px apart, and every box inset in this file
     is one of them - see the thesis at the top. Not colours, so like --rail
     they are declared once and have no dark-mode twin. Four is enough because
     this app has exactly four kinds of gap: inside a chip, between a label and
     its control, inside a dense row, and inside a card. A fifth step is how a
     scale becomes a list of numbers again. */
  --pad-1: 4px;
  --pad-2: 8px;
  --pad-3: 12px;
  --pad-4: 16px;
  /* The rail's width is a token because three unrelated rules depend on it - the
     shell's grid track, the drawer's off-canvas transform, and the width the
     drawer opens to - and a rail that is 15rem in one of them and 240px in
     another is a rail that slides in half-covered. Not a colour, so it lives
     here only and has no dark-mode twin. */
  --rail: 15rem;
  /* The collapsed rail. 4.5rem is not a round number picked for looks: it is a
     40px hit target plus a --pad-2 either side plus the 1px border, which is
     the narrowest a column of icons can be and still be pressable with a thumb.
     A token because the shell's grid track TRANSITIONS between the two widths,
     and a transition between two numbers that disagree is a rail that settles
     at the wrong width. */
  --rail-narrow: 4.5rem;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* Prose stops at a readable line length everywhere, including the
     methodology, which is the longest text in the app. */
  --measure: 68ch;
}

/* Dark is a real theme, not an inversion. The paper goes to a warm-neutral
   near-black rather than pure black so the hairlines still read, and the ink
   stops short of pure white because #fff on near-black buzzes at the small
   sizes this app uses.

   IT IS REACHABLE TWO WAYS, AND IT HAS TO BE BOTH.
   The device asks for dark with prefers-color-scheme. A member asks for it in
   their account menu, which sets data-theme on <html>. This file used to carry
   only the media query, so applyTheme() set an attribute nothing styled: a
   member on a light laptop who chose Dark got a fully light page with dark
   scrollbars, and app.js's comment calling data-theme "the hook styles.css
   hangs its override on" described a hook that did not exist.

   These are custom-property declarations on :root, so they compete only with
   the light block above and never with the hundreds of rules below - which is
   why the attribute selector is used plainly here. An earlier attempt wrapped
   them in :where() to "keep specificity at zero" and thereby made them LOSE to
   the plain :root above: the page stayed light whatever was chosen. Specificity
   is the mechanism that makes this work, not a hazard to be neutralised.

   The :not([data-theme="light"]) is what lets an explicit Light choice win on a
   device set to dark. Choosing Light and staying dark is the half people
   notice. */
:root[data-theme="dark"] {

  --bg: #12161b;
  --surface: #191e25;
  --surface-sunken: #1f252d;
  --line: #333c47;
  --line-soft: #262d36;
  --ink: #e8edf3;
  --ink-muted: #a3aebd;
  /* Same argument as the light theme's, same measurement: was #7e8898, which
  is 4.3:1 on the sunken ground under a hovered row. Now 6.0:1 on
  --surface, 6.5:1 on --bg, 5.6:1 on --surface-sunken, 5.5:1 on
  --accent-soft. */
  --ink-faint: #929cab;

  --accent: #8ab4f8;
  --accent-ink: #10151b;
  --accent-soft: #1b2733;
  --accent-line: #2f4459;

  --ok: #7ec99b;
  --ok-soft: #16241c;
  --warn: #dfb570;
  --warn-soft: #241f16;
  --danger: #f0a99b;
  --danger-soft: #2a1c19;
  --danger-line: #4d3029;

  --seal: #c4a8e0;
  --seal-soft: #221c2b;
  --seal-line: #3c3149;

  /* Same hues as light, lower lightness. Contrast against --core-ink:
  Happy 5.0, Fearful 5.8, Bad 5.8, Disgusted 5.5, Sad 6.2, Angry 6.4,
  Surprised 6.8 - all clear of 4.5:1. */
  --core-happy: #73672b;
  --core-surprised: #614785;
  --core-bad: #37674a;
  --core-fearful: #735a2b;
  --core-angry: #8b413c;
  --core-disgusted: #616161;
  --core-sad: #3e5c7e;
  --core-ink: #eef2f7;
  --core-edge: rgba(232, 237, 243, .22);

  --focus: #8ab4f8;
  --scrim: rgba(0, 0, 0, .6);
  --lift: 0 6px 24px rgba(0, 0, 0, .5);

}

/* The same palette, for a device that asked for dark while the member has not
   said otherwise. Written out a second time because CSS cannot put a media
   query inside a selector list and this app has no preprocessor. THE TWO
   BLOCKS MUST STAY IN STEP - changing one alone is the bug the comment above
   exists to prevent. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {

  --bg: #12161b;
  --surface: #191e25;
  --surface-sunken: #1f252d;
  --line: #333c47;
  --line-soft: #262d36;
  --ink: #e8edf3;
  --ink-muted: #a3aebd;
  /* Same argument as the light theme's, same measurement: was #7e8898, which
  is 4.3:1 on the sunken ground under a hovered row. Now 6.0:1 on
  --surface, 6.5:1 on --bg, 5.6:1 on --surface-sunken, 5.5:1 on
  --accent-soft. */
  --ink-faint: #929cab;

  --accent: #8ab4f8;
  --accent-ink: #10151b;
  --accent-soft: #1b2733;
  --accent-line: #2f4459;

  --ok: #7ec99b;
  --ok-soft: #16241c;
  --warn: #dfb570;
  --warn-soft: #241f16;
  --danger: #f0a99b;
  --danger-soft: #2a1c19;
  --danger-line: #4d3029;

  --seal: #c4a8e0;
  --seal-soft: #221c2b;
  --seal-line: #3c3149;

  /* Same hues as light, lower lightness. Contrast against --core-ink:
  Happy 5.0, Fearful 5.8, Bad 5.8, Disgusted 5.5, Sad 6.2, Angry 6.4,
  Surprised 6.8 - all clear of 4.5:1. */
  --core-happy: #73672b;
  --core-surprised: #614785;
  --core-bad: #37674a;
  --core-fearful: #735a2b;
  --core-angry: #8b413c;
  --core-disgusted: #616161;
  --core-sad: #3e5c7e;
  --core-ink: #eef2f7;
  --core-edge: rgba(232, 237, 243, .22);

  --focus: #8ab4f8;
  --scrim: rgba(0, 0, 0, .6);
  --lift: 0 6px 24px rgba(0, 0, 0, .5);

}
}


/* ---- base ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font: 15px/1.55 var(--font);
  background: var(--bg);
  color: var(--ink);
  -webkit-text-size-adjust: 100%;
  /* THE PAGE NEVER SCROLLS SIDEWAYS. `clip` rather than `hidden` because hidden
     would make the top bar's sticky positioning stop working. Anything wider
     than the viewport must scroll inside its own .tablewrap instead. */
  overflow-x: clip;
}
h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.01em; line-height: 1.25; }
/* The scale, stated once. h2 used to be omitted here and set ad hoc by whichever
   block remembered, so a card heading rendered at the inherited body size on
   every page that did not - the headings in a document library and the headings
   in the budget were two different typographic ideas. */
h1 { font-size: 22px; }
h2 { font-size: 16px; }
h3 { font-size: 14.5px; }
h4 { font-size: 13px; }
p { margin: 0 0 12px; max-width: var(--measure); }
button { font: inherit; color: inherit; cursor: pointer; }
input, select, textarea { font: inherit; color: inherit; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
hr { border: 0; border-top: 1px solid var(--line-soft); margin: 20px 0; }
/* One focus ring for the whole app, and it is visible. Six people using a
   keyboard on a laptop in a hotel is the normal case, not the edge case. */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }

/* ---- shell ---- */
.topbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 16px;
  padding: 0 var(--pad-4); height: 56px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
/* index.html ships the bar empty. While app.js has not filled the nav there is
   no member yet - boot, sign-in, or a hard error - and a chrome bar over a
   sign-in card is furniture pretending the app is open. */
.topbar:has(#nav:empty) { display: none; }
/* EVERY RULE BELOW IS SCOPED TO .topbar ON PURPOSE. The navigation moved to the
   side rail, and the rail reuses two of these names - .brand for the forum's
   name, .nav for the list of sections. Left unscoped, `.brand { white-space:
   nowrap }` clipped a two-line forum name in the rail and `.nav { display:flex }`
   laid the rail's sections out as a horizontal strip inside a 15rem column. The
   bar itself still ships in index.html and still hides itself while #nav is
   empty, which is now always; these rules stay so that page keeps working if it
   is ever filled again. */
.topbar .brand { font-weight: 700; font-size: 16px; white-space: nowrap; letter-spacing: -.01em; }
.topbar .nav { display: flex; gap: 2px; flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.topbar .nav::-webkit-scrollbar { display: none; }
.topbar .nav a, .topbar .nav button {
  padding: 6px 12px; border: 0; border-radius: 999px; background: none;
  color: var(--ink-muted); font-weight: 600; font-size: 14px; white-space: nowrap;
}
.topbar .nav a:hover, .topbar .nav button:hover { color: var(--accent); text-decoration: none; }
.topbar .nav a.active, .topbar .nav a[aria-current="page"], .topbar .nav button.active {
  background: var(--accent-soft); color: var(--accent);
}
.rail-foot .me, .menu-who .me { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--ink-muted); white-space: nowrap; }

/* The measure and the padding belong to the PAGE, not to .main. .main is the
   column the rail sits beside and it must be able to run full-bleed - the
   drawer's scrim and a future sticky sub-header both need its edges - while the
   words inside it stop at a readable width. Written without #app even though
   index.html gives .main that id: an id here would outrank every later rule
   that wants to adjust a page's padding, including the phone one below. */
/* The gutter is one step beyond --pad-4, written as a sum so it is visibly on
   the same ladder as everything inside it: 24px of page gutter against a card's
   16px inset is a ratio the eye reads as nesting. It was 26px, which is not a
   step of anything. */
.main > .page {
  max-width: 1080px; margin: 0 auto;
  padding: calc(var(--pad-4) + var(--pad-2)) calc(var(--pad-4) + var(--pad-2)) 76px;
}
.boot { padding: 22vh 0; text-align: center; color: var(--ink-faint); max-width: none; }
.page-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 0 0 20px; }
.page-head h1 { font-size: 21px; }
.page-head .sub { color: var(--ink-muted); font-size: 13.5px; }
.page-head .head-actions { margin-left: auto; display: inline-flex; gap: 8px; align-self: center; }
.section { margin: 0 0 34px; }
.section > h2 {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .09em;
  color: var(--ink-muted); margin: 0 0 10px;
}

/* ---- primitives ---- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.card + .card { margin-top: 14px; }
.card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: var(--pad-3) var(--pad-4); border-bottom: 1px solid var(--line-soft);
  font-weight: 650; font-size: 14px;
}
.card-body { padding: var(--pad-4); }
.card-foot {
  padding: var(--pad-3) var(--pad-4); border-top: 1px solid var(--line-soft);
  display: flex; gap: var(--pad-2);
}
/* THE LAST THING IN A BOX DOES NOT GET TO ADD TO THE BOX'S OWN INSET.
   Almost everything in this file carries a bottom margin so it can be followed
   by something else - a <p> has 12px, a .field 14px, a .banner 16px - and as
   the last child of a card body that margin lands ON TOP of the body's 16px,
   so the card is visibly deeper below its content than above it. It is the
   single most common spacing fault in the app because it needs two correct
   rules to happen; this is the one place to answer it for all of them. */
.card-body > :last-child, .stack > :last-child, .modal-body > :last-child { margin-bottom: 0; }
.stack { display: flex; flex-direction: column; gap: 12px; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; align-items: start; }
/* THE GAP OWNS THE RHYTHM, THE SAME ARGUMENT .editor MAKES FURTHER DOWN. A
   .field carries a 14px bottom margin so it can be followed by another one; in
   a grid whose gap is already 14px that margin lands underneath the last row
   as well, so a two-field form in a .grid-2 sits 28px off the bottom of its
   card and visibly sags. */
.grid-2 > .field { margin-bottom: 0; }
.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }
.small { font-size: 13px; }
.mono { font-family: var(--mono); font-size: 12.5px; }
.nums { font-variant-numeric: tabular-nums; }
.prose { max-width: var(--measure); line-height: 1.65; }

.btn {
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  border-radius: var(--radius-sm); padding: 7px 14px; font-weight: 600; font-size: 14px;
  display: inline-flex; align-items: center; gap: 7px; text-decoration: none;
}
.btn:hover { border-color: var(--ink-faint); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn.primary:hover { filter: brightness(1.08); }
.btn.danger { color: var(--danger); border-color: var(--danger-line); }
.btn.danger:hover { background: var(--danger-soft); }
.btn.quiet { border-color: transparent; background: none; color: var(--ink-muted); }
.btn.quiet:hover { background: var(--surface-sunken); color: var(--ink); }
.btn.small { padding: 4px 10px; font-size: 13px; }
/* A disabled button must look unavailable rather than merely pale: half the
   buttons in this app are disabled for a REASON (nothing to submit yet, someone
   else's reflection), and .btn[disabled] + a .hint beside it is that sentence. */
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.btn[disabled]:hover, .btn[aria-disabled="true"]:hover { border-color: var(--line); filter: none; background: var(--surface); }
.btn.primary[disabled]:hover { background: var(--accent); }

.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 14px; }
.field > label {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-muted);
}
.field input, .field select, .field textarea, .input {
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 9px 11px; background: var(--surface); color: var(--ink); width: 100%;
  /* A SELECT AND THE INPUT BESIDE IT MUST BE THE SAME HEIGHT, and left alone
     they are not: a text input inherits the body's 1.55 line-height while a
     <select> uses the UA's `normal`, so the same padding produced 41.7px and
     38.5px side by side in the members table - a 3px step that reads as one
     control sitting slightly low rather than as two different line-heights.
     Both are stated here so neither depends on the UA's opinion. */
  line-height: 1.55;
  min-height: 2.7rem;
}
/* An <option> inherits the select's colours in some engines and the UA's in
   others; a dark theme is where that shows, as a white dropdown over a dark
   page. Named so the answer is the same in both. */
.field select option, .input option { background: var(--surface); color: var(--ink); }
.field textarea { resize: vertical; min-height: 74px; line-height: 1.55; }
.field .hint { font-size: 12.5px; color: var(--ink-muted); }
.field.invalid input, .field.invalid textarea, .field.invalid select { border-color: var(--danger); }
.field.invalid .hint { color: var(--danger); }
.req { color: var(--danger); font-weight: 700; }
/* The character/word budget the methodology imposes (7 words for an event,
   ~7 per significance line) shown as a count, going red only when it is over. */
.count { font-size: 12px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.count.over { color: var(--danger); font-weight: 700; }

.chip {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  border-radius: 999px; padding: 2px 10px; font-size: 12px; font-weight: 650;
  background: var(--surface-sunken); color: var(--ink-muted);
}
.chip.accent { background: var(--accent-soft); color: var(--accent); }
.chip.ok { background: var(--ok-soft); color: var(--ok); }
.chip.warn { background: var(--warn-soft); color: var(--warn); }
.chip.danger { background: var(--danger-soft); color: var(--danger); }
.chip.seal { background: var(--seal-soft); color: var(--seal); }
.avatar {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-sunken); color: var(--ink-muted);
  font-size: 11px; font-weight: 700;
}

/* Any table that can outgrow the viewport lives in one of these.
   overscroll-behavior-x stops a thumb that reaches the end of a sideways scroll
   from handing the gesture to the browser and navigating back out of the app -
   which, mid-way through writing a reflection, loses the draft. */
.tablewrap {
  overflow-x: auto; overscroll-behavior-x: contain;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
}
/* SEPARATE, NOT COLLAPSE, and the reason is the sticky first column further
   down. Under border-collapse the borders belong to the table rather than to
   the cells, so a sticky cell scrolls while its own border stays behind - the
   Updates table lost its first-column rule the moment it was scrolled sideways.
   With border-spacing: 0 nothing moves visually: only border-bottom is ever
   declared here, so there is no pair of borders left to double up. */
.table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 14px; }
/* ONE PADDING FOR THE HEADER AND THE CELL, and it is the card's inset, so a
   column of cells lines up with the words in the card above the table rather
   than sitting two pixels in from them. Written as one selector because a
   header padded differently from its own column is the fault that makes a
   sorted table look very slightly skewed. */
.table th, .table td {
  text-align: left; padding: var(--pad-3) var(--pad-4);
  border-bottom: 1px solid var(--line-soft); vertical-align: top;
}
.table thead th {
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em; font-weight: 700;
  color: var(--ink-muted); border-bottom: 1px solid var(--line);
}
.table tbody tr:last-child td { border-bottom: 0; }

/* ---- the four states every screen has ---- */

/* LOADING. A skeleton rather than a spinner: this app's screens are lists and
   tables, and a grey ghost of the list that is coming is calmer than a spinner
   that says only "wait". */
.loading { padding: 4px 0; }
.skeleton {
  height: 12px; border-radius: 999px; background: var(--surface-sunken);
  margin-bottom: 10px; max-width: 100%;
  animation: pulse 1.5s ease-in-out infinite;
}
.skeleton.wide { width: 100%; }
.skeleton.half { width: 52%; }
.skeleton.short { width: 28%; }
.skeleton.tall { height: 62px; border-radius: var(--radius); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* EMPTY. Left-aligned prose, not a centred shrug with an icon. An empty screen
   in this app almost always means "nobody has written this yet", which is a
   sentence worth writing out, usually with the button that fixes it. */
.empty {
  padding: var(--pad-4); color: var(--ink-muted);
  font-size: 14px; line-height: 1.6; max-width: var(--measure);
}
.empty .empty-t { font-weight: 650; color: var(--ink); margin-bottom: var(--pad-1); }
/* The button that fixes an empty screen sits on its own line under the
   sentence, whatever order the markup puts it in. */
.empty .btn { display: flex; width: fit-content; margin-top: var(--pad-3); }
/* Standing alone in a table it needs the air; standing in a card body it does
   NOT, because the body has already given it 16px on all four sides and the
   sentence would otherwise start 32px from the card's edge - which is where an
   empty state stops looking calm and starts looking lost. app.js reaches for
   card('', emptyBlock(...)) on nine screens, so this is the common case. */
.table .empty, .tablewrap .empty { padding: calc(var(--pad-4) + var(--pad-2)) var(--pad-4); }
.card-body > .empty, .card-body > .stack > .empty { padding: 0; }

/* ERROR. The server's error protocol returns a sentence naming the fix, so the
   banner is built to carry a sentence, not a code. */
.banner {
  border: 1px solid var(--line); border-left: 3px solid var(--ink-faint);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--surface-sunken); padding: var(--pad-3) var(--pad-4); margin: 0 0 var(--pad-4);
  font-size: 14px; line-height: 1.55; max-width: var(--measure);
}
.banner .banner-t { font-weight: 700; margin-bottom: 3px; }
.banner p:last-child { margin-bottom: 0; }
.banner.error { background: var(--danger-soft); border-color: var(--danger-line); border-left-color: var(--danger); color: var(--danger); }
.banner.error .banner-t { color: var(--danger); }
.banner.warn { background: var(--warn-soft); border-left-color: var(--warn); }
.banner.ok { background: var(--ok-soft); border-left-color: var(--ok); }
.banner.info { background: var(--accent-soft); border-color: var(--accent-line); border-left-color: var(--accent); }
.error-page { padding: 12vh 20px; max-width: 560px; margin: 0 auto; text-align: left; }
.error-page h1 { font-size: 20px; margin-bottom: 8px; }

/* READ-ONLY. Someone else's shared reflection, a submitted form, a past
   meeting. The rule: read-only content keeps its LAYOUT and loses its
   CHROME - no boxes that invite a cursor, no buttons that do nothing. A field
   nobody can type in must not look like a field. */
.readonly .field input, .readonly .field textarea, .readonly .field select,
.ro-value {
  border-color: transparent; background: none; padding-left: 0; padding-right: 0;
}
.readonly .field > label { color: var(--ink-faint); }
.ro-value { display: block; white-space: pre-wrap; }
.ro-note {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--ink-muted); margin-bottom: 14px;
}
.locked { opacity: .72; }

/* ---- the confidentiality mark ----
   THE ONE THING A MEMBER MUST NEVER HAVE TO GUESS is who can read what is on
   the screen. Every reflection view carries this band, and it says one of two
   things in two different colours: sealed (only the author) or shared with the
   forum. Anything that changes that state is a deliberate, named action. */
.seal {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--seal-line); border-left: 3px solid var(--seal);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--seal-soft); color: var(--seal);
  padding: var(--pad-3) var(--pad-4); margin: 0 0 var(--pad-4);
  font-size: 13.5px; font-weight: 600;
}
.seal .seal-t { font-weight: 700; }
.seal .seal-note { color: var(--ink-muted); font-weight: 400; }
.seal .btn { margin-left: auto; }
.seal.shared {
  border-color: var(--accent-line); border-left-color: var(--accent);
  background: var(--accent-soft); color: var(--accent);
}

/* ---- the 5% form ----
   Five columns, in the order of the Word form: area, its one rating, then for
   each of the two entries the event (10%), the significance (80%) and the
   feelings (10%). The 80% column is the widest because it is the work. */
.five-wrap { overflow-x: auto; }
.five { width: 100%; border-collapse: collapse; min-width: 880px; font-size: 14px; }
.five th, .five td {
  text-align: left; vertical-align: top; padding: var(--pad-3) var(--pad-4);
  border-bottom: 1px solid var(--line-soft);
}
.five thead th {
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em; font-weight: 700;
  color: var(--ink-muted); border-bottom: 1px solid var(--line); white-space: nowrap;
}
.five thead th .pct { color: var(--ink-faint); font-weight: 600; letter-spacing: 0; text-transform: none; }
/* The area and rating cells are vertically merged across the pair, exactly as
   the Word table merges them - one rating per area, shared by best and worst. */
.five .c-area, .five .c-rate { border-right: 1px solid var(--line-soft); }
.five .c-area { width: 124px; font-weight: 650; }
/* Wide enough for the ten-button scale to sit as two rows of five. A narrower
   column turns it into a vertical column of ten, which reads as a list of
   options rather than as a scale. */
.five .c-rate { width: 176px; }
.five .c-event { width: 21%; font-weight: 600; }
.five .c-signif { min-width: 280px; }
.five .c-feel { width: 200px; }
/* An empty cell says its sentence in the cell, not in a padded panel. */
.five .empty { padding: 0; font-size: 13.5px; }
/* The pair reads as one block: a hairline between best and worst, a full rule
   between areas, so an eye scanning the page sees four areas, not eight rows. */
.five tr.worst td { border-bottom: 1px solid var(--line); }
.five tr:last-child td { border-bottom: 0; }

.entry-lbl {
  display: inline-block; border-radius: 999px; padding: 1px 9px; margin-bottom: 6px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
}
/* Best and worst are labelled because the completed paper forms are not
   consistent about which row is which, and a reader guessing from the tone of
   the sentence is a reader who can guess wrong about somebody's worst month. */
.entry-lbl.best { background: var(--ok-soft); color: var(--ok); }
.entry-lbl.worst { background: var(--warn-soft); color: var(--warn); }

.rating-value { font-size: 26px; font-weight: 700; letter-spacing: -.03em; line-height: 1.1; }
.rating-of { color: var(--ink-faint); font-size: 12.5px; font-weight: 600; }
.rating-scale { display: grid; grid-template-columns: repeat(5, 30px); gap: 4px; }
.rating-pick {
  width: 30px; height: 30px; padding: 0; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--surface);
  font-size: 13px; font-weight: 650; font-variant-numeric: tabular-nums;
}
.rating-pick:hover { border-color: var(--ink-faint); }
.rating-pick[aria-pressed="true"], .rating-pick.on {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}

/* The three "so what" lines are numbered because they are a drill, not a list:
   line 2 is the significance OF line 1. Numbering them is the only cue in the
   layout that says the order carries meaning. */
.signif { list-style: none; margin: 0; padding: 0; counter-reset: sowhat; }
.signif li {
  counter-increment: sowhat; position: relative; padding-left: 22px;
  margin-bottom: 6px; line-height: 1.5; max-width: var(--measure);
}
.signif li::before {
  content: counter(sowhat); position: absolute; left: 0; top: 1px;
  color: var(--ink-faint); font-size: 11px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.signif li:last-child { margin-bottom: 0; }
.signif input { margin-bottom: 6px; }

/* ---- feelings: chips and picker ----
   The colour is the point. On the printed wheel a member finds a word by its
   colour first and reads it second, and the app has to keep that true or the
   picker becomes a dropdown of 96 words. */
.feelings { display: flex; flex-wrap: wrap; gap: 5px; }
.feeling {
  display: inline-flex; align-items: baseline; gap: 5px; white-space: nowrap;
  border-radius: 999px; padding: 3px 10px; font-size: 12.5px; font-weight: 650;
  background: var(--surface-sunken); color: var(--core-ink);
  border: 1px solid var(--core-edge);
}
/* The stored value is the full path {core, secondary, tertiary}: three tertiary
   words sit under two cores, so the colour can only come from the core. */
.feeling.core-happy { background: var(--core-happy); }
.feeling.core-surprised { background: var(--core-surprised); }
.feeling.core-bad { background: var(--core-bad); }
.feeling.core-fearful { background: var(--core-fearful); }
.feeling.core-angry { background: var(--core-angry); }
.feeling.core-disgusted { background: var(--core-disgusted); }
.feeling.core-sad { background: var(--core-sad); }
.feeling .via { font-size: 11px; font-weight: 500; opacity: .75; }
.feeling .drop { border: 0; background: none; color: inherit; padding: 0 0 0 2px; font-size: 13px; line-height: 1; opacity: .6; }
.feeling .drop:hover { opacity: 1; }

/* Four slots, because the methodology says four - shown empty rather than
   absent so the form states the requirement without a validation message. */
.feel-slots { display: flex; flex-wrap: wrap; gap: 6px; }
.feel-slot {
  border: 1px dashed var(--line); border-radius: 999px; padding: 3px 12px;
  font-size: 12.5px; color: var(--ink-faint);
  /* A <button> with no background takes the browser's default grey, which in
     the dark theme puts --ink-faint on light grey - measured under 3:1 and the
     only control in the app that failed. Every other pill sets its own ground;
     this one was relying on inheriting a page it does not sit on. */
  background: var(--surface);
  cursor: pointer;
}
.feel-slot:hover { border-style: solid; color: var(--ink-muted); }
.feel-slot:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.wheel { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; }
.wheel-core { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.wheel-core > h4 {
  padding: var(--pad-2) var(--pad-3); font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--core-ink); background: var(--surface-sunken);
}
.wheel-core.core-happy > h4 { background: var(--core-happy); }
.wheel-core.core-surprised > h4 { background: var(--core-surprised); }
.wheel-core.core-bad > h4 { background: var(--core-bad); }
.wheel-core.core-fearful > h4 { background: var(--core-fearful); }
.wheel-core.core-angry > h4 { background: var(--core-angry); }
.wheel-core.core-disgusted > h4 { background: var(--core-disgusted); }
.wheel-core.core-sad > h4 { background: var(--core-sad); }
.wheel-sec { padding: var(--pad-2) var(--pad-3) var(--pad-3); }
.wheel-sec > .sec-name { font-size: 11.5px; font-weight: 700; color: var(--ink-muted); margin-bottom: 4px; }
.wheel-opt {
  display: block; width: 100%; text-align: left; border: 0; background: none;
  padding: 5px 8px; border-radius: var(--radius-sm); font-size: 13.5px; color: var(--ink);
}
.wheel-opt:hover { background: var(--surface-sunken); }
.wheel-opt[aria-pressed="true"], .wheel-opt.on { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
.wheel-opt[disabled] { opacity: .4; cursor: not-allowed; }

/* ---- meetings, agenda, documents, parking lot ---- */
.list-row {
  display: flex; align-items: center; gap: var(--pad-3);
  padding: var(--pad-3) var(--pad-4);
  border-bottom: 1px solid var(--line-soft);
}
.list-row:last-child { border-bottom: 0; }
.list-row .grow { flex: 1; min-width: 0; }
.list-row .title { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-row.clickable { cursor: pointer; }
.list-row.clickable:hover { background: var(--surface-sunken); }

/* A FULL-WIDTH CONTROL IN A FLEX ROW IS A FLEX-BASIS OF 100%, AND THAT IS HOW
   A FIELD DISAPPEARS.
   .input carries `width: 100%` because nine times out of ten it is a block in a
   .field. Put one directly in a .list-row and that width becomes its
   flex-basis: the control asks for the whole row, .grow beside it asks for 0
   (flex: 1 with min-width: 0), and since an item with a zero basis absorbs none
   of the deficit when a row overflows, ALL the space goes to the loose control
   and none to .grow. Measured in Chrome at 1280px on the forums page: the
   organisation box took 776px and the forum-name box inside .grow came out
   24px wide - its padding and borders, with no content box at all. It looks
   like a decorative sliver, so an admin meaning to rename the forum types into
   the only box they can see and renames the organisation instead.
   The fix is at the row, not at the field: a control that is a DIRECT child of
   a row sizes to its content and takes a share of the slack, and a control
   inside .grow keeps its full-width block behaviour. */
.list-row > .input { flex: 0 1 14rem; width: auto; min-width: 0; }
.list-row .grow > .input { flex: none; width: 100%; }

/* The two named fields on the forums row. Neither had a rule of its own, which
   is how the bug above went unnoticed for as long as it did: nothing in this
   file mentioned either box, so nothing said what either box was for.
   The name is the row's title and is set like one - it is the same string the
   .title above it shows on the forums an admin cannot rename. The organisation
   is a short badge of a word ("YPO"), so it gets a track it cannot outgrow
   rather than a share of the slack. */
.list-row .f-name { font-weight: 650; }
.list-row > .f-org { flex: 0 1 11rem; }

/* A ROW INSIDE A .stack IS ALREADY INSET BY WHATEVER HOLDS THE STACK.
   .list-row is built for a bare card, where rows butt against each other, the
   hairline is the separator and the 16px is the card's only inset. Dropped into
   a .stack inside a .card-body it inherits neither of those truths: the body
   insets it 16px and the row insets itself 16 more, so the rows start 32px in
   while the paragraph above them starts at 16 - measured, on the forums, roles
   and "discussed repeatedly" lists. And the stack's 12px gap arrives on top of
   the row's own hairline, which is the two-devices-for-one-job the Updates
   table further down is written against: a rule floating in the middle of a
   gap belongs to neither row.
   So in a stack the row gives up the padding that is not its to add, and the
   stack gives up the gap the hairline is already doing. */
.stack > .list-row { padding-left: 0; padding-right: 0; }
.stack:has(> .list-row) { gap: 0; }
.date-block {
  flex: none; min-width: 46px; text-align: center; padding: var(--pad-1) var(--pad-2);
  border-radius: var(--radius-sm); background: var(--surface-sunken); color: var(--ink-muted);
}
.date-block .d { display: block; font-size: 17px; font-weight: 700; line-height: 1.1; }
.date-block .m { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
.date-block.next { background: var(--accent-soft); color: var(--accent); }
.roles { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13px; color: var(--ink-muted); }
.roles b { color: var(--ink); font-weight: 650; }
.agenda .slot { flex: none; min-width: 96px; font-family: var(--mono); font-size: 12.5px; color: var(--ink-muted); }
.agenda .lead { flex: none; color: var(--ink-muted); font-size: 13px; }
/* The parking lot's two axes (urgent or not; being or doing) are the whole
   point of it, so they are chips on the row rather than a column somewhere. */
.park-row .cats { display: flex; gap: 5px; flex: none; }
.doc-row .kind { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); text-transform: uppercase; }
.methodology { max-width: var(--measure); }
.methodology h3 { font-size: 14px; margin: 22px 0 5px; }
.methodology h3:first-child { margin-top: 0; }
.methodology p { color: var(--ink-muted); line-height: 1.65; }

/* ---- sign in ---- */
.signin { min-height: 82vh; display: flex; align-items: center; justify-content: center; padding: calc(var(--pad-4) + var(--pad-2)) var(--pad-4); }
.signin-card {
  width: 420px; max-width: 100%; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
  /* 28px, the same as the server-rendered sign-in card at the bottom of this
     file. There are two sign-in cards in this app - the SPA's and the one the
     auth pages render outside it - and a member who is bounced from one to the
     other sees the same box twice. It was 30px here and 28px there, which is
     not a difference anyone can name and is exactly the kind that makes the
     second page feel like a different app. */
  padding: calc(var(--pad-4) + var(--pad-3));
}
.signin-card h1 { font-size: 22px; margin-bottom: 4px; }
.signin-card .sub { color: var(--ink-muted); font-size: 14px; margin-bottom: 22px; }
.signin-card .btn { width: 100%; justify-content: center; }
.signin-or {
  display: flex; align-items: center; gap: 12px; margin: 18px 0;
  color: var(--ink-faint); font-size: 12px; text-transform: uppercase; letter-spacing: .08em;
}
.signin-or::before, .signin-or::after { content: ""; flex: 1; border-top: 1px solid var(--line-soft); }

/* ---- modal and toasts ---- */
.modal-wrap {
  position: fixed; inset: 0; z-index: 70; background: var(--scrim);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 8vh var(--pad-4); overflow-y: auto;
}
.modal { width: 560px; max-width: 100%; background: var(--surface); border-radius: var(--radius); box-shadow: var(--lift); }
/* A modal is a card that floats, so it is inset like one. It used to run
   18/20/14/12/4 down its three parts, which is five numbers for one box and
   left the head and the foot a different distance from the same edge. */
.modal-head { padding: var(--pad-4) var(--pad-4) 0; font-size: 17px; font-weight: 700; }
.modal-body { padding: var(--pad-3) var(--pad-4); }
.modal-foot {
  padding: var(--pad-3) var(--pad-4) var(--pad-4);
  display: flex; justify-content: flex-end; gap: var(--pad-2);
}
#toasts { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 100; display: flex; flex-direction: column; gap: 8px; }
.toast {
  background: var(--ink); color: var(--bg); border-radius: var(--radius-sm);
  padding: 10px 18px; font-size: 13.5px; font-weight: 600; box-shadow: var(--lift);
  animation: toast-in .16s ease-out;
}
.toast.err { background: var(--danger); color: var(--surface); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---- the phone ----
   Below 760px the 5% table stops being a table. Each entry becomes a card with
   its cells labelled, because a member reading their own worst quarter on a
   phone must not have to scroll sideways to find out which column they are in.
   The label comes from the cell's class, so this works whatever app.js does;
   a data-label attribute, if one is set, wins over it. */
@media (max-width: 760px) {
  .main > .page { padding: var(--pad-4) var(--pad-3) 60px; }
  .topbar { padding: 0 var(--pad-3); gap: 10px; }
  .five-wrap { overflow-x: visible; }
  .five { min-width: 0; display: block; font-size: 14.5px; }
  .five thead { display: none; }
  .five tbody, .five tr, .five td { display: block; }
  .five tr {
    border: 1px solid var(--line); border-radius: var(--radius);
    background: var(--surface); margin-bottom: 14px; padding: 4px 0;
  }
  /* The worst row is the second half of the same area, so it joins the card
     above it rather than starting a new one: the pair shares one rating and
     splitting them would make that rating look like it belongs to the best
     entry alone. The dashed rule is the seam. */
  .five tr.best { border-bottom: 0; border-radius: var(--radius) var(--radius) 0 0; margin-bottom: 0; }
  .five tr.worst { border-top: 1px dashed var(--line); border-radius: 0 0 var(--radius) var(--radius); }
  /* Written at the same weight as the desktop width rules they undo - a
     one-class `.five td` loses to `.five .c-event` and the cell keeps a 21%
     width it can no longer honour, which is how "Signed the Geneva mandate"
     ends up set one word per line. */
  .five td,
  .five .c-area, .five .c-rate, .five .c-event, .five .c-signif, .five .c-feel {
    display: block; border: 0; padding: var(--pad-2) var(--pad-4); width: auto; min-width: 0;
  }
  .five .c-area, .five .c-rate { border-right: 0; }
  .five tr.worst td { border-bottom: 0; }
  .five td::before {
    display: block; margin-bottom: 3px;
    font-size: 10.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
    color: var(--ink-faint);
  }
  .five .c-area::before { content: "Area"; }
  .five .c-rate::before { content: "Rating"; }
  .five .c-event::before { content: "Event · 10%"; }
  .five .c-signif::before { content: "Significance · 80%"; }
  .five .c-feel::before { content: "Feelings · 10%"; }
  .five td[data-label]::before { content: attr(data-label); }
  .five .c-area { font-size: 16px; padding-top: 12px; }
  .five .c-rate { display: flex; align-items: baseline; gap: 8px; }
  .five .c-rate::before { margin-bottom: 0; }
  .rating-value { font-size: 20px; }
  .wheel { grid-template-columns: 1fr; }
  .modal-wrap { padding: 0; align-items: flex-end; }
  /* THE SHEET HAS TO BE ABLE TO SCROLL. Bottom-sheeting the modal without a
     height bound left the emotion wheel - seven cores, forty-one branches -
     taller than the viewport with no way to reach the bottom of it, on the one
     screen size every member uses it from. The wheel is the control the whole
     5% form depends on. */
  .modal { border-radius: var(--radius) var(--radius) 0 0; max-height: 88vh; display: flex; flex-direction: column; }
  .modal > .modal-body, .modal .sheet-body { overflow-y: auto; -webkit-overflow-scrolling: touch; }
  .page-head .head-actions { margin-left: 0; width: 100%; }
}

/* On a phone the top bar has room for the wordmark or for the sections, not
   both: left alone, four of the six nav pills sit off the right edge behind a
   scroll with no affordance, which is the same as not existing. The wordmark
   goes first - the tab, the icon and the page head all already say whose forum
   this is, and nobody signed in is wondering. */
@media (max-width: 560px) {
  .topbar .brand { display: none; }
  .me .who { display: none; }
}

/* A member reading this on a phone in a lobby has one hand and a small screen.
   Nothing in this app needs motion to be understood, so anyone who has asked
   the OS for less of it gets none. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}

/* ---------------------------------------------------------------------------
 * The emotion wheel's colour, carried by class.
 *
 * WHY THIS BLOCK EXISTS RATHER THAN AN INLINE style ATTRIBUTE
 * The colour is the wheel: seven cores, each with a hue the members already
 * recognise from the printed sheet. The picker first set those colours inline,
 * which the app's own CSP (`style-src 'self'`, no 'unsafe-inline') refused -
 * so every tile rendered identical and grey while nothing appeared to be
 * broken. Keeping the policy strict is worth more than the shortcut, so each
 * core sets one custom property here and the level classes below decide how
 * much of it to show.
 * ------------------------------------------------------------------------- */
.c-happy     { --core: var(--core-happy); }
.c-surprised { --core: var(--core-surprised); }
.c-bad       { --core: var(--core-bad); }
.c-fearful   { --core: var(--core-fearful); }
.c-angry     { --core: var(--core-angry); }
.c-disgusted { --core: var(--core-disgusted); }
.c-sad       { --core: var(--core-sad); }

/* Each level of the wheel shows progressively less of the core's colour, so
 * depth reads as shade rather than as another label to parse. The flat colour
 * is declared first so a browser without color-mix still gets a coloured tile
 * rather than a white one. */
.tile.core      { background: var(--core); background: color-mix(in srgb, var(--core) 70%, transparent); border-color: var(--core); }
.tile.secondary { background: var(--core); background: color-mix(in srgb, var(--core) 45%, transparent); border-color: var(--core); }
.tile.tertiary  { background: var(--core); background: color-mix(in srgb, var(--core) 24%, transparent); border-color: var(--core); }
.feel-chip      { background: var(--core); background: color-mix(in srgb, var(--core) 30%, transparent); border-color: var(--core); }
.feel-chip .dot { background: var(--core); }

/* The organisation sits beside the forum's name as a quiet badge, not as part
 * of it. Without this rule the span renders flush against the name and the
 * masthead reads "La Côte ForumYPO". */
.topbar .brand .org {
  margin-left: .5rem;
  padding: .1rem .4rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  font-size: .68em;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--ink-muted);
  vertical-align: middle;
}

/* A field whose controls belong on one line: the two parking-lot axes, where
 * the choice is between two named options rather than free text.
 *
 * Without these rules `.field`'s column layout applied, so each radio sat
 * centred on its own line above its own label - which read as four unrelated
 * controls instead of two either/or choices, and gave no clue that "urgent" and
 * "not urgent" were the same question. The methodology asks for one category on
 * each of two axes, so the form should look like two questions. */
.field.row {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
}
.field.row .label-ish {
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--ink-muted);
  margin-right: 2px;
}
.field.row .radio {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13.5px;
  text-transform: none;
  letter-spacing: 0;
  white-space: nowrap;
  cursor: pointer;
}
/* The inputs must not inherit .field's full-width block sizing, which is what
 * stretched each radio across the card and forced the stacking above. */
.field.row .radio input[type="radio"] {
  width: auto;
  margin: 0;
  accent-color: var(--accent, currentColor);
}

/* ---------------------------------------------------------------------------
 * The standalone auth pages: sign in, "check your email", and the dead-link
 * message. Server-rendered, outside the SPA, and therefore outside every layout
 * rule above.
 *
 * THIS IS THE FIRST THING ALL SIX MEMBERS EVER SEE
 * It shipped using classes that did not exist - .plain, .button, .fine, .notice
 * had no rules at all, and the app's button class is .btn - so the page rendered
 * as a full-bleed card with the label, field and an unreadable light-on-light
 * button crammed onto one line. A sign-in screen that looks broken is a sign-in
 * screen people do not trust with the most private thing they will ever type.
 * ------------------------------------------------------------------------- */
body.plain {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: calc(var(--pad-4) + var(--pad-2));
  background: var(--bg);
  color: var(--ink);
}
body.plain .card {
  width: 100%;
  max-width: 26rem;
  padding: calc(var(--pad-4) + var(--pad-3));   /* the SPA's .signin-card, exactly */
  border: 1px solid var(--line);
  border-radius: var(--radius, 10px);
  background: var(--surface, var(--bg));
}
body.plain h1 { margin: 0 0 6px; font-size: 22px; letter-spacing: -.01em; }
body.plain .muted { margin: 0 0 20px; color: var(--ink-muted); font-size: 14px; }
body.plain form { display: flex; flex-direction: column; gap: 6px; }
body.plain label {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-muted);
}
body.plain input[type="email"] {
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm, 7px);
}
/* Full width and stacked, because on a phone these are the only two controls on
 * the screen and either of them being a thumb-miss is a member who cannot get
 * in and has nobody to ask. */
.btn.block { display: block; width: 100%; text-align: center; margin-top: 10px; }
body.plain .or {
  margin: 14px 0 10px;
  text-align: center;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ink-muted);
}
body.plain .fine { margin: 16px 0 0; font-size: 12.5px; color: var(--ink-muted); }
body.plain .notice {
  margin: 0 0 16px;
  padding: 10px 12px;
  border-left: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: 0 var(--radius-sm, 7px) var(--radius-sm, 7px) 0;
  font-size: 13.5px;
}
body.plain p { line-height: 1.55; }

/* The label is hidden from sight, not from assistive technology.
 *
 * The sign-in box is one field and its purpose is obvious to anyone who can see
 * the placeholder - but a placeholder is not a label: it disappears the moment
 * you type, it is not announced reliably, and a form control with no label is
 * one a screen reader reads as "edit text, blank". Hiding it this way rather
 * than with display:none is the whole point, because display:none would take it
 * out of the accessibility tree too. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The quieter of two doors. Small, centred, and plainly a link rather than a
 * button, because most of the people signing in here cannot use it. */
body.plain .alt { margin: 18px 0 0; text-align: center; font-size: 13px; }
body.plain .alt a { color: var(--ink-muted); text-decoration: underline; text-underline-offset: 3px; }
body.plain .alt a:hover, body.plain .alt a:focus-visible { color: var(--ink); }

/* ===========================================================================
 * THE SHELL: A SIDE RAIL AND ONE SCROLLING COLUMN
 *
 * The sections used to be pills in a top bar, and a top bar is a fixed budget:
 * the rule above this one had already had to hide the wordmark on a phone to
 * fit six of them, and four still sat off the right edge behind a scroll with
 * no affordance, which is the same as not existing. A vertical rail has room
 * for the sections this app will ever have, room for the forum's name above
 * them, and room for the signed-in member below them - and it puts the answer
 * to "whose forum am I looking at" in the same place on every screen, which is
 * the thing that had to change now that one person can belong to more than one.
 *
 * THE RAIL IS PINNED, THE COLUMN SCROLLS, AND THE PAGE OWNS THE SCROLLBAR.
 * The obvious build is `height: 100dvh` on the shell with `overflow-y: auto` on
 * .main. It looks identical and it is worse: the browser then has no page scroll
 * to hide the phone's address bar into, every anchor and every restore-scroll
 * lands on the wrong element, and iOS rubber-bands the whole frame. So the shell
 * is an ordinary tall document and the rail is `position: sticky` - it does not
 * move with the content, but the content is still the page.
 * ======================================================================== */
.shell {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100dvh;
}
/* index.html ships both slots empty, which is exactly the signed-out shape:
   boot, sign-in, session expiry, hard error. An empty 15rem column beside a
   sign-in card is furniture pretending the app is open - the same argument the
   old .topbar:has(#nav:empty) rule was written for, and the same shape. */
.sidebar:empty { display: none; }
.shell:has(.sidebar:empty) { grid-template-columns: minmax(0, 1fr); }

.sidebar {
  /* align-self is load-bearing: a grid item defaults to stretch, and a stretched
     item is already as tall as its track, so sticky has nothing to travel in and
     the rail scrolls away with the page. */
  align-self: start;
  position: sticky;
  top: 0;
  height: 100dvh;
  z-index: 50;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  border-right: 1px solid var(--line);
}

/* The forum's name, which is the point of the rail. Two lines, not one: the
   name is a place ("La Côte Forum") and the organisation is a fact about it
   ("YPO"), and setting them at the same size makes the second look like part of
   the first. .brand's base rules live under .topbar now, so nothing here is
   undoing a nowrap that would have clipped a long name to one line. */
.sidebar .brand {
  flex: none;
  padding: var(--pad-4);
  border-bottom: 1px solid var(--line-soft);
}
.sidebar .brand .forum-name {
  display: block;
  font-size: 15.5px; font-weight: 700; line-height: 1.25; letter-spacing: -.01em;
  color: var(--ink);
}
.sidebar .brand .forum-org {
  display: block; margin-top: 3px;
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink-muted);
}

/* If app.js wraps the sections in a <nav>, that wrapper takes the slack and the
   rail-foot stays pinned; if it does not, .sidebar's own overflow catches it.
   Either shape works, which is the point of writing both. */
.sidebar nav { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.sidebar .nav-item {
  display: flex; align-items: center; gap: 10px;
  position: relative;
  flex: none;
  /* A generous hit area, because this is the control a member uses most and
     often one-handed on a phone. 40px is the height, not the text's - the
     padding is doing that job rather than a line-height nobody can see - and
     it is 40 rather than 38 because that is the floor the collapsed rail
     promises, and a hit area that shrinks when the labels go is the opposite
     of what collapsing is for. */
  min-height: 40px;
  margin: 1px var(--pad-2);
  padding: var(--pad-2) var(--pad-3);
  border: 0; border-radius: var(--radius-sm);
  background: none;
  color: var(--ink-muted);
  font: inherit; font-weight: 600; font-size: 14px;
  text-align: left; text-decoration: none;
}
.sidebar .nav-item:hover { background: var(--surface-sunken); color: var(--ink); text-decoration: none; }
/* THE CURRENT SECTION IS NOT MARKED BY COLOUR ALONE. --accent-soft against
   --surface is a four-percent shift in lightness; it survives a laptop on a
   desk and it does not survive a phone held at an angle in sunlight, let alone
   a reader who cannot separate the two hues. So "you are here" is a tint AND a
   bar down the left edge AND a heavier weight, and any one of the three carries
   it on its own. */
.sidebar .nav-item.on,
.sidebar .nav-item[aria-current="page"] {
  background: var(--accent-soft); color: var(--accent); font-weight: 700;
}
.sidebar .nav-item.on::before,
.sidebar .nav-item[aria-current="page"]::before {
  content: ""; position: absolute; left: -8px; top: 5px; bottom: 5px;
  width: 3px; border-radius: 0 3px 3px 0; background: var(--accent);
}
.sidebar .nav-item .nav-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The rail's items sit flush to its left edge, so the app-wide 2px focus offset
   would draw the ring half-off the rail. Inset instead. */
.sidebar .nav-item:focus-visible { outline-offset: -2px; }

.sidebar .rail-foot {
  flex: none;
  margin-top: auto;                 /* pinned to the bottom, however few sections there are */
  padding: var(--pad-3) var(--pad-4);
  border-top: 1px solid var(--line-soft);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 13px; color: var(--ink-muted);
}
.sidebar .rail-foot .who { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.sidebar .rail-foot .btn { padding: var(--pad-1) 10px; font-size: 12.5px; }

/* ---- the icon in a nav item ----
 * app.js inlines a 24-unit stroke SVG per section, painted in currentColor so
 * it takes the item's own colour in both themes and in the on state without a
 * rule. Three things it needs from this file, and only three.
 *
 * IT MUST NOT SHRINK. It is a flex item beside a label that can be long, and
 * flex items shrink by default: "Discussed repeatedly" squeezed a 20px icon to
 * 17 and the icons in the column stopped agreeing about size, which reads as
 * six icons drawn at six weights.
 *
 * IT MUST NOT SIT ON THE TEXT BASELINE. An inline SVG is replaced content and
 * gets the line-box's descender gap under it, so the row grew by three pixels
 * and the glyph rode high against its label. display:block inside a flex item
 * takes the baseline out of the question entirely.
 *
 * AND IT IS ALIGNED OPTICALLY, NOT ARITHMETICALLY. These are stroked outlines
 * in a 24-unit box with about a unit of air on each side, so a box centred on
 * the cap height looks a hair high; the half-pixel nudge is what puts the
 * drawn mass on the middle of the word. */
.sidebar .nav-icon {
  flex: none;
  display: block;
  width: 20px; height: 20px;
  transform: translateY(.5px);
}

/* The brand row holds two things: the forum's name and the control that
   collapses the rail. flex-start rather than centre, so a two-line forum name
   keeps the chevron level with its FIRST line - where the eye expects a
   control - instead of floating it down beside the organisation.
   THE COLLAPSE CONTROL IS ALWAYS VISIBLE AND ALWAYS REACHABLE, which is the
   whole contract of a control that can hide the thing it lives in. It is a
   real <button>, so it gets a real hit area (36px square - the one thing in
   the rail allowed under 40, because it sits alone in a corner with nothing to
   mis-hit) and the app's focus ring, inset like the nav items' for the same
   reason: the rail's edge is right there. No border and no ground until it is
   hovered - at rest it is a chevron in a corner, not a seventh thing competing
   with six section names. The negative margin pulls its generous hit area back
   towards the brand's own 16px inset, so the icon looks aligned with the edge
   the name is aligned with rather than floating short of it. */
.sidebar .brand {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--pad-2);
}
.sidebar .brand-name { min-width: 0; }
.sidebar .rail-collapse {
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; padding: 0; margin: -6px -6px 0 0;
  border: 0; border-radius: var(--radius-sm);
  background: none; color: var(--ink-muted);
}
.sidebar .rail-collapse:hover { background: var(--surface-sunken); color: var(--ink); }
.sidebar .rail-collapse:focus-visible { outline-offset: -2px; }
.sidebar .rail-collapse .nav-icon { flex: none; transform: none; }

/* ===========================================================================
 * THE COLLAPSED RAIL
 *
 * The rail gives back its width and keeps its function: icons only, centred,
 * labels gone, sub-navigation gone. It is the member's explicit choice - the
 * chevron does this and navigating never does, so nothing moves out from under
 * a finger that was aiming at it.
 *
 * IT IS ONE TOKEN AND THE GRID TRACK, NOT A WIDTH ON THE RAIL. .shell is a
 * two-column grid, so if the rail alone narrowed, the main column would keep
 * its old track and the page would sit in a 15rem-wide hole. Transitioning the
 * TRACK moves the rail and the column as one thing, which is the difference
 * between a rail that slides and a page that jumps.
 *
 * THE LABELS GO WITH display:none AND THAT IS DELIBERATE. Hiding them with a
 * width or an opacity leaves them in the flex row, so the icons stay wherever
 * a zero-width label left them instead of centring. app.js gives the item a
 * title and an aria-label when it is collapsed, so the name is still there for
 * a pointer and for a screen reader - the icon is never the only cue.
 *
 * NOTHING HERE IS BELOW 40px. The hit area is the thing collapsing is most
 * likely to cost, so it is stated rather than left to whatever the padding
 * happens to sum to.
 * ======================================================================== */
.shell { transition: grid-template-columns .18s ease; }
body.rail-collapsed .shell { grid-template-columns: var(--rail-narrow) minmax(0, 1fr); }

body.rail-collapsed .sidebar .nav-label,
body.rail-collapsed .sidebar .subnav { display: none; }
body.rail-collapsed .sidebar .nav-item {
  justify-content: center;
  gap: 0;
  padding-left: 0; padding-right: 0;
  /* BOTH dimensions, on every item in the rail. The floor used to be height
     only, and the horizontal padding is stripped one line above - so the
     profile link at the foot, whose content is a 20px icon, measured 20x40.
     That is under the 24px WCAG minimum, on the only route into the account
     page from a collapsed rail, aimed at with a thumb. */
  min-height: 40px;
  min-width: 40px;
}
body.rail-collapsed .sidebar .rail-foot { justify-content: center; padding-left: 0; padding-right: 0; }
body.rail-collapsed .sidebar .rail-foot .btn,
body.rail-collapsed .sidebar .rail-foot .nav-item { min-width: 40px; min-height: 40px; justify-content: center; }
/* The forum's name is a sentence, not a glyph. At 4.5rem minus its padding it
   has about 40px to live in, which turns "La Côte Forum" into a stack of
   broken syllables - worse than absent, because it looks like a rendering
   fault. So the brand goes and the control that brings it back stays, centred
   and alone, which is also the only thing left up there to press. */
body.rail-collapsed .sidebar .brand { justify-content: center; padding-left: 0; padding-right: 0; }
body.rail-collapsed .sidebar .brand-name { display: none; }
body.rail-collapsed .sidebar .rail-collapse { margin: -6px 0 0; }

/* ===========================================================================
 * THE SUB-NAVIGATION IN THE RAIL
 *
 * Updates has three views of one set of rows, and while that section is open
 * they hang under it in the rail. They are a DIFFERENT KIND OF CHOICE from the
 * sections above them and must not look like one: smaller, lighter, quieter,
 * and hung off a spine so the indent is a fact about the tree rather than an
 * amount of whitespace someone chose.
 *
 * THE SPINE LANDS UNDER THE ICON'S CENTRE, and the number is arithmetic rather
 * than taste: the nav item is 8px out from the rail (its margin) plus 12px of
 * its own padding, and the icon is 20px wide, so its centre is at 30. A line
 * dropped there reads as descending from the section itself. Any other number
 * reads as an indent that nearly lines up with something.
 *
 * THE CURRENT VIEW IS CURRENT WITHOUT SHOUTING. The rail's sections use the
 * accent - tint, bar and weight - and the subnav must not, or two things on
 * the same column both claim to be where you are. So it darkens to full ink
 * and gains weight and a sunken ground: three cues, none of them the accent.
 * ======================================================================== */
.sidebar .subnav {
  display: flex; flex-direction: column;
  margin: var(--pad-1) var(--pad-2) var(--pad-2) 30px;
  padding-left: 10px;
  border-left: 1px solid var(--line);
}
.sidebar .subnav-item {
  display: flex; align-items: center;
  min-height: 30px;
  padding: var(--pad-1) 10px;
  border-radius: var(--radius-sm);
  color: var(--ink-muted);
  font-size: 13px; font-weight: 600;
  text-decoration: none;
}
.sidebar .subnav-item:hover { background: var(--surface-sunken); color: var(--ink); text-decoration: none; }
.sidebar .subnav-item.on,
.sidebar .subnav-item[aria-current="page"] {
  background: var(--surface-sunken); color: var(--ink); font-weight: 700;
}
.sidebar .subnav-item:focus-visible { outline-offset: -2px; }

.main { min-width: 0; }
/* On a laptop the rail is always there, so the button that opens it is not.
   Declared here rather than as a min-width companion to the drawer block below,
   because a pair of media queries meeting at 900px leaves a sub-pixel band at
   899.5 that neither one claims - and the symptom is a Menu button that appears
   for no reason on a resized window. */
.sidebar-toggle { display: none; }

/* The drawer. Below 900px a 15rem rail is a third of the screen, so it leaves.
 *
 * IT SLIDES ON `left`, NOT ON `transform`, AND THAT IS DELIBERATE.
 * A transformed element becomes the containing block for its fixed-position
 * descendants, so the Menu button - which app.js may well paint inside the rail
 * it opens - would ride off-screen with the closed drawer and there would be no
 * way back. Animating `left` costs nothing at this size and keeps the viewport
 * as the frame of reference for everything inside.
 *
 * visibility:hidden rather than opacity or a bare offset, because an off-screen
 * rail that is still in the tab order is a keyboard trap where Tab walks into
 * six invisible links. It is transitioned alongside `left` so it flips at the
 * END of the closing slide instead of blanking the rail mid-move.
 */
@media (max-width: 899px) {
  .shell, .shell:has(.sidebar:empty) { grid-template-columns: minmax(0, 1fr); }
  /* COLLAPSING IS A LAPTOP IDEA AND IT IS UNDONE HERE. Below this width the
     rail is a drawer that is off-screen until it is asked for, so there is no
     width to give back - and a member who collapsed the rail on a laptop and
     then narrowed the window would open a full-width drawer showing nothing
     but a column of icons. The class survives (it is remembered per device and
     the laptop layout is one resize away), so its two effects are taken back
     rather than the state being cleared. */
  body.rail-collapsed .shell { grid-template-columns: minmax(0, 1fr); }
  body.rail-collapsed .sidebar .nav-label { display: inline; }
  body.rail-collapsed .sidebar .nav-item { justify-content: flex-start; gap: 10px; padding: var(--pad-2) var(--pad-3); }
  body.rail-collapsed .sidebar .brand { justify-content: space-between; padding-left: var(--pad-4); padding-right: var(--pad-4); }
  body.rail-collapsed .sidebar .brand-name { display: block; }
  body.rail-collapsed .sidebar .rail-collapse { margin: -6px -6px 0 0; }
  body.rail-collapsed .sidebar .rail-foot { justify-content: flex-start; padding-left: var(--pad-4); padding-right: var(--pad-4); }
  .sidebar {
    position: fixed;
    top: 0; bottom: 0;
    left: calc(-1 * var(--rail) - 2rem);
    width: min(var(--rail) + 2rem, 84vw);
    height: 100dvh;
    visibility: hidden;
    box-shadow: var(--lift);
    transition: left .2s ease, visibility .2s;
  }
  body.rail-open .sidebar { left: 0; visibility: visible; }
  /* The scrim is a pseudo-element on <body> rather than a div, because there is
     no scrim in the class contract and inventing one would be markup the other
     half of this app does not know to emit. A click on it lands on <body>,
     which is where a delegated close handler already listens.

     AND THE PAGE BEHIND IT STAYS SCROLLABLE. The reflex is to add
     `body { overflow: hidden }` alongside this; on iOS that throws away the
     scroll position, so a member who opens the menu, changes their mind and
     closes it is returned to the top of the reflection they were halfway
     through reading. The drawer contains its OWN overscroll instead (see
     .sidebar above), which is the part that actually misbehaves. */
  body.rail-open::before {
    content: "";
    position: fixed; inset: 0; z-index: 45;
    background: var(--scrim);
  }
  .sidebar-toggle {
    /* Fixed, and visible even when its parent is not, so the button works
       wherever app.js chooses to paint it - including inside the closed rail. */
    display: inline-flex; align-items: center; gap: 7px;
    position: fixed; top: 10px; left: 10px; z-index: 60;
    visibility: visible;
    /* Given its own chrome rather than relying on .btn: this is the one control
       that must never be invisible, and whether app.js also gives it .btn is
       not something this file gets to assume. */
    padding: 7px 12px;
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--ink);
    font: inherit; font-weight: 650; font-size: 13.5px;
    box-shadow: var(--lift);
  }
  .main { padding-top: 46px; }         /* clearance for the fixed Menu button */
  /* The same clearance inside the rail, because the button does not move when
     the drawer opens - it sits over the rail's own top-left corner, where it
     was covering the first half of "La Côte Forum". Opening a drawer to read
     which forum you are in, and finding the name behind the button you just
     pressed, is a small thing that happens every single time. */
  .sidebar .brand { padding-top: 54px; }
}

/* ===========================================================================
 * SUB-NAVIGATION INSIDE A PAGE
 * The rail carries the six sections; Updates carries three views of one set of
 * rows. Those are different kinds of choice and they must not look alike, so
 * these are underlined tabs rather than a second set of pills. The underline is
 * also the colourblind-safe half of the .on state.
 * ======================================================================== */
.subtabs {
  display: flex; flex-wrap: wrap; gap: 2px;
  margin: 0 0 18px;
  border-bottom: 1px solid var(--line-soft);
}
.subtab {
  border: 0; background: none;
  padding: var(--pad-2) var(--pad-3);
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  color: var(--ink-muted); font: inherit; font-weight: 600; font-size: 13.5px;
  text-decoration: none; white-space: nowrap;
}
.subtab:hover { color: var(--ink); text-decoration: none; }
.subtab.on, .subtab[aria-selected="true"] {
  color: var(--ink); font-weight: 700; border-bottom-color: var(--accent);
}

/* ===========================================================================
 * THE UPDATES TABLE
 *
 * One table replaces three pages, so it has to survive being read rather than
 * skimmed: rows tall enough to track across, ONE separator (a hairline, not a
 * hairline plus a zebra - two devices for one job just makes the table louder),
 * a header that admits it is clickable, and grouping that reads as a heading
 * rather than as a row with no data in it.
 * ======================================================================== */
.table tbody tr:hover td { background: var(--surface-sunken); }
.table th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.table th.sortable:hover { color: var(--ink); }
/* A sortable header must be operable from the keyboard, so app.js may well put
   a <button> in the th. It must not then look like a button: this strips the UA
   chrome and hands back the header's own type, so the markup can be accessible
   without the table growing six grey rectangles along the top. */
.table th.sortable > button {
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit;
  letter-spacing: inherit; text-transform: inherit; text-align: inherit;
  cursor: pointer;
}
/* The caret is the affordance AND the state. Faint and double-headed when the
   column is merely sortable, solid and directional when it is the sort - so the
   direction survives without colour, which the tint alone would not. */
.table th.sortable::after {
  content: "\2195"; margin-left: 6px; font-size: 10px; color: var(--ink-faint);
}
.table th.sorted-asc::after, .table th[aria-sort="ascending"]::after {
  content: "\25B2"; margin-left: 6px; font-size: 9px; color: var(--accent);
}
.table th.sorted-desc::after, .table th[aria-sort="descending"]::after {
  content: "\25BC"; margin-left: 6px; font-size: 9px; color: var(--accent);
}
.table th.sorted-asc, .table th.sorted-desc { color: var(--ink); }

/* A group header is not a row of the data, it is a label over some of it. */
.table tr.group-row td, .table tr.group-row th {
  background: var(--surface-sunken);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding-top: var(--pad-2); padding-bottom: var(--pad-2);
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-muted);
}
.table tr.group-row:first-child td, .table tr.group-row:first-child th { border-top: 0; }
.table tr.group-row:hover td { background: var(--surface-sunken); }

.table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* THE FIRST COLUMN STAYS WHEN THE TABLE SCROLLS SIDEWAYS. A table cannot
   reflow into cards the way the 5% form does - these rows are being COMPARED,
   and comparison is the one thing a stack of cards cannot do - so on a phone it
   scrolls inside .tablewrap instead, and the column that says which row you are
   on has to come with you. The separating rule is a box-shadow rather than a
   border because it must sit on the outside edge of a cell that is painting
   over its neighbours; a border-right would be underneath them. */
.table th:first-child, .table td:first-child {
  position: sticky; left: 0; z-index: 1;
  background: var(--surface);
  box-shadow: 1px 0 0 var(--line-soft);
}
.table thead th:first-child { z-index: 2; }
.table tbody tr:hover td:first-child { background: var(--surface-sunken); }
.table tr.group-row td:first-child, .table tr.group-row th:first-child {
  /* A group header usually spans the whole width. Pinned, its text would sit
     stubbornly at the left edge while the label it belongs to scrolled away. */
  position: static; box-shadow: none; background: var(--surface-sunken);
}

/* ===========================================================================
 * FILTERS
 * A wrapping row, because the number of filters differs per page and a fixed
 * grid would leave a hole on the page with three of them.
 * ======================================================================== */
.filters {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 14px;
  margin: 0 0 16px;
}
.filter-field { display: flex; flex-direction: column; gap: var(--pad-1); min-width: 0; }
/* A GROUP OF PILLS IS NOT A LAYOUT UNTIL SOMEBODY GIVES IT ONE.
   The kind filter is a labelled <div role="group"> of .filter-chip buttons,
   and that div had no rule at all: a block box holding inline-flex children
   that app.js joins with no whitespace between them. Measured at 1280px, the
   three chips came out edge to edge - chip one ending at 308.4 and chip two
   starting at 308.4 - so a row of separate either/or choices read as a single
   ruled strip, and the selected one looked like a segment of a control rather
   than the one pill that is on. Written against the role rather than a class
   name, because the group is the ARIA shape app.js already emits and there is
   no class on it to hook. */
.filter-field > [role="group"] { display: flex; flex-wrap: wrap; gap: var(--pad-2); min-width: 0; }
.filter-field label {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-muted);
}
/* Not .field's full-width block sizing: these sit side by side, and a select
   stretched to the card's width beside two others is three rows of one control. */
.filter-field select, .filter-field input {
  width: auto; min-width: 9.5rem; max-width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 7px 10px;
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 13.5px;
}
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 4px 12px;
  background: var(--surface); color: var(--ink-muted);
  font: inherit; font-size: 12.5px; font-weight: 650;
  text-decoration: none;
}
.filter-chip:hover { border-color: var(--ink-faint); color: var(--ink); text-decoration: none; }
/* On is a tint, a solid accent edge and a heavier weight - the same three-cue
   rule the rail's current section follows, for the same reason. */
.filter-chip.on, .filter-chip[aria-pressed="true"] {
  background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 700;
}

/* ===========================================================================
 * THE ANNUAL CALENDAR
 *
 * Twelve months on one screen, which is the view a forum actually plans in:
 * retreats, the two-day meetings and the summer gap are a SHAPE, and the shape
 * is the answer to "when could we do this". A month grid on a page of them has
 * to survive being small, so almost every decision here is about not wobbling.
 *
 * SIX ROWS, ALWAYS. A month needs four to six week-rows depending on which
 * weekday it starts on, so a grid sized to its contents makes February short
 * and March tall and the whole page shuffles as the year changes. The day grid
 * is therefore given a fixed 7:6 aspect ratio and six equal rows: cells come out
 * square, every month is the same height, and a five-row month simply has an
 * empty row rather than a different geometry.
 *
 * NO COLUMN GAP, AND THAT IS THE WHOLE TRICK. A multi-day meeting is drawn by
 * filling the cells it covers; with a gap between columns a 6-9 September
 * retreat renders as four separate dots and reads as four separate events. The
 * cells touch, the fill runs edge to edge, and only the first and last are
 * rounded - so it reads as one band. Rows still get a gap, because a band has
 * no business running down the page into the following week.
 * ======================================================================== */
.year-nav { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; }
/* The class contract names .year-nav but not the element that holds the year
   itself, so every plausible shape is covered. min-width in ch stops the
   arrows from shuffling sideways when the label goes from 2025 to a shorter or
   wider string, which on a control you press repeatedly is the difference
   between paging through years and chasing a button.
   .nums IS IN THE LIST BECAUSE THAT IS WHAT THE MARKUP ACTUALLY EMITS. The
   year rides in a <span class="nums">, so the rule below was styling nothing
   at all and the year between the two arrows was rendering at the body's 15px
   with no fixed track - which is the shuffle this rule exists to stop. Naming
   .year as well is not belt-and-braces: it is the name in the class contract,
   and the next hand to touch that markup will reach for it. */
.year-nav .year, .year-nav .nums, .year-nav > strong {
  font-size: 19px; font-weight: 700; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  min-width: 4.5ch; text-align: center;
}

.year-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 22px 20px; }
@media (max-width: 1180px) { .year-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px)  { .year-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .year-grid { grid-template-columns: minmax(0, 1fr); gap: 20px; } }

.month { min-width: 0; }
.month h4 {
  margin: 0 0 6px; padding-left: 2px;
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-muted);
}
.weekday-row {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  margin-bottom: 3px;
}
.weekday {
  text-align: center;
  font-size: 9.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-muted);
  overflow: hidden;
}
.day-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  grid-template-rows: repeat(6, minmax(0, 1fr));
  column-gap: 0;
  row-gap: 2px;
  aspect-ratio: 7 / 6;
  min-width: 0;
}
.day {
  display: grid; place-items: center;
  min-width: 0; padding: 0;
  border: 0; border-radius: 999px;
  background: transparent;
  color: var(--ink);
  font: inherit; font-size: 11.5px; font-variant-numeric: tabular-nums; line-height: 1;
  -webkit-appearance: none; appearance: none;
}
.day .day-num { line-height: 1; }
/* Recedes, but stays READABLE. This wanted --ink-faint and could not have it,
   because that token was 3.0:1 on paper and a day number is text. The token now
   clears 4.5:1 on every ground in this file (see the palette), so the rule can
   finally say what it meant: the previous month's days step back one tier, and
   someone deciding whether the 1st is a Monday can still read them. */
.day.other-month { color: var(--ink-faint); font-weight: 400; }
.day.today { box-shadow: inset 0 0 0 2px var(--ink); font-weight: 700; }
.day.has-meeting { background: var(--accent); color: var(--accent-ink); font-weight: 650; }
/* Measured, not eyeballed: --accent-ink on --accent is 6.5:1 on paper and 8.7:1
   in the dark theme. This pair is the one most likely to fail, because it is
   the only place in the app where a number sits ON the accent. */
.day.today.has-meeting { box-shadow: inset 0 0 0 2px var(--accent-ink); }

/* The band. Order matters and is not alphabetical: .in-range squares both ends,
   then the two caps round their own outer edge, and the last rule catches the
   single-day meeting that carries BOTH caps - without it the .range-end rule
   wins outright and a one-day meeting renders flat on its left side. */
.day.in-range { border-radius: 0; }
.day.range-start { border-radius: 999px 0 0 999px; }
.day.range-end { border-radius: 0 999px 999px 0; }
.day.range-start.range-end { border-radius: 999px; }

.day-legend {
  display: flex; flex-wrap: wrap; gap: 8px 18px;
  margin-top: 18px;
  font-size: 12px; color: var(--ink-muted);
}
.day-legend > * { display: inline-flex; align-items: center; gap: 6px; }
/* A .day used as a legend swatch is outside the grid that gives it its size. */
.day-legend .day { width: 14px; height: 14px; flex: none; font-size: 0; }

/* On a phone the Updates table is wider than the screen, and it must ADMIT that
 * rather than compress six columns into a 380px card where "Reflection shared
 * with the forum" sets one word per line. A floor on the table's width forces
 * .tablewrap to scroll itself; the page body still never moves sideways, which
 * is the rule the whole file is built around. The 5% form is the exception and
 * it is handled its own way, further up: those rows are read one at a time, so
 * they become cards. These rows are COMPARED, so they stay a table. */
@media (max-width: 760px) {
  .tablewrap .table { min-width: 40rem; }
  .table th, .table td { padding: var(--pad-2) var(--pad-3); }
  .subtabs { margin-bottom: 14px; }
  .filters { gap: 8px 10px; }
  .filter-field select, .filter-field input { min-width: 8rem; }
  .year-nav { margin-bottom: 14px; }
}

/* ===========================================================================
 * ROLES, AND THE SENTENCE THAT REPLACES A CONTROL
 *
 * A role is not a colour, it is an amount of authority, so these four chips are
 * an EMPHASIS LADDER rather than four hues: member is an outline, treasurer and
 * moderator are tints, admin is solid ink. Read down the roster and the loudest
 * chip is the person who can do the most, which is the only fact anyone is
 * actually looking for in that column.
 *
 * THE LADDER SURVIVES GREYSCALE AND COLOURBLINDNESS, BECAUSE HUE IS THE THIRD
 * CUE, NOT THE FIRST. Each step differs in fill (none, tint, tint, solid), in
 * weight, and in the shape of its leading dot (ring, disc, square, disc on
 * solid). Photocopy this page, or hand it to the roughly one member in twelve
 * who cannot separate red from green, and it still reads. Every pair below was
 * measured: 5.8:1 (member), 5.8:1 (treasurer), 5.7:1 (moderator) and 14.7:1
 * (admin) on paper; 7.5, 8.2, 7.2 and 15.4 in the dark theme.
 *
 * NOTHING HERE IS A PERMISSION. The role chip says who somebody is; it never
 * decides what is on the screen. That is boot.can's job, and app.js branches on
 * it - a stylesheet that hid controls by role name would be a second, silent
 * copy of the rules the server already resolved.
 * ======================================================================== */
.role-chip {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  border: 1px solid var(--line); border-radius: 999px;
  padding: 2px 10px 2px 8px;
  font-size: 11.5px; font-weight: 600; letter-spacing: .01em;
  background: none; color: var(--ink-muted);
  vertical-align: baseline;
}
/* The dot is drawn from currentColor so a variant only has to name one colour,
   and its SHAPE is the cue that outlives the colour. */
.role-chip::before {
  content: ""; flex: none;
  width: 8px; height: 8px; border-radius: 50%;
  background: currentColor;
}
.role-member { background: none; border-color: var(--line); color: var(--ink-muted); font-weight: 600; }
.role-member::before { background: none; border: 2px solid currentColor; }

.role-treasurer {
  background: var(--ok-soft); color: var(--ok); font-weight: 700;
  border-color: color-mix(in srgb, var(--ok) 40%, transparent);
}

/* Square, not round, and that is the whole difference from treasurer at a
   glance in black and white. */
.role-moderator {
  background: var(--accent-soft); color: var(--accent); font-weight: 700;
  border-color: var(--accent-line);
}
.role-moderator::before { border-radius: 2px; }

/* The only solid chip in the app, on purpose: admin is the only role that can
   change everybody else's. --ink on --bg is the pair the toast already uses, so
   it is one that has been looked at in both themes. NOT --seal - that token
   means "only you can see this" and confusing the two is the one thing the
   colour vocabulary at the top of this file forbids. */
.role-admin {
  background: var(--ink); color: var(--bg); border-color: var(--ink); font-weight: 700;
}

/* ---- the roster's editable cells ----
 * Four controls in one table row - name, email, forum, role - two of them text
 * boxes and two of them selects, and the row has to read as ONE row of one
 * height. The selects had no rule of their own and were relying entirely on
 * .input, which is a class the markup half may reasonably drop from a control
 * this file has never mentioned; named here so the roster is right either way.
 * The height they share comes from .input's line-height and min-height, which
 * is where the 3px step between a select and the input beside it was fixed. */
.roster .m-forum, .roster .m-role, .m-forum, .m-role {
  width: 100%; min-width: 0;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 9px 11px; line-height: 1.55; min-height: 2.7rem;
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 14px;
}
/* The in-memoriam chip follows a full-width input inside the name cell, so it
   wraps onto the line below it - and it was touching the box above by half a
   pixel, which reads as a label attached to the field rather than a fact about
   the member. It is inline after a <strong> in the read-only roster, where it
   must NOT gain a margin, so this is scoped to the case that wraps. */
.roster .input + .chip { margin-top: var(--pad-1); }

/* THE REFUSAL IS A NOTE, NOT AN ERROR. It appears where a control would have
   been - "Adding a meeting needs the meeting.manage permission" - and the
   member has done nothing wrong, so it gets none of the error vocabulary: no
   --danger, no tinted panel, no 3px bar. A quiet hairline and muted ink say
   "this is an explanation" in the same breath as saying what to ask for.
   app.js sets the permission id in a <strong>; mono makes it look like the
   thing you would quote to an admin rather than an emphasis. */
.perm-note {
  margin: 0 0 14px;
  padding-left: var(--pad-3);
  border-left: 2px solid var(--line);
  max-width: var(--measure);
  font-size: 13px; line-height: 1.55; color: var(--ink-muted);
}
.perm-note strong, .perm-note code {
  font-family: var(--mono); font-size: .92em; font-weight: 600;
  color: var(--ink); background: var(--surface-sunken);
  padding: 0 4px; border-radius: 4px;
}
.empty .perm-note, .card-body > .perm-note:last-child { margin-bottom: 0; }

/* ===========================================================================
 * THE MEETING EDITOR
 *
 * A meeting is one object with a dozen fields and an agenda under it, and the
 * form has to stay a FORM - a column of labelled questions - rather than
 * becoming a dashboard of boxes. So .editor owns the vertical rhythm (one gap,
 * everywhere) and .editor-row is the only thing allowed to put two questions
 * side by side, which it stops doing the moment there is no room.
 *
 * THE GAP REPLACES .field's MARGIN rather than adding to it. Left alone, a
 * flex column with a 14px gap around fields that each carry a 14px bottom
 * margin spaces the last pair 28px and the rest 14px, and the form visibly
 * sags at the bottom.
 * ======================================================================== */
.editor { display: flex; flex-direction: column; gap: 14px; }
.editor .field { margin-bottom: 0; }
/* Same argument one level up: the editor is a column of cards or sections, and
   both of those carry their own bottom margin from elsewhere in this file. Left
   alone the meeting editor spaced its cards 28px and its fields 14px, which
   reads as two unrelated forms on one page. ONE gap owns the rhythm. */
.editor > .card + .card { margin-top: 0; }
.editor > .section { margin-bottom: 0; }
/* A fieldset is the right element for "the three meeting roles" and the wrong
   one by default: the UA border and inline-ish sizing turn a group of fields
   into a box inside a card, and a fieldset that will not shrink below its
   content is what breaks a grid column on a phone. */
.editor fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.editor legend {
  padding: 0; margin-bottom: 6px;
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-muted);
}

/* auto-fit rather than a fixed column count, because the rows differ - two
   dates, three roles, a venue and its link - and minmax(min(100%, 13rem), 1fr)
   is what makes the stack on a phone automatic instead of a media query per
   shape. The min() matters: a bare 13rem minimum overflows a 320px screen. */
.editor-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  gap: 14px;
  align-items: start;
}
.editor-row > * { min-width: 0; }

/* THE PRIMARY ACTION IS LAST BY EYE AND FIRST BY KEYBOARD, AND `order` IS HOW
   BOTH ARE TRUE AT ONCE. Save belongs at the right end of the footer, where
   this platform's buttons live; it also belongs early in the tab order,
   because it is what the member came to do. `order` moves the paint without
   moving the DOM, so whichever order app.js emits, Save ends up right-most and
   whatever the markup put first is still what Tab reaches first. Only the
   primary is reordered - one button, one exception, nobody stranded.
   A destructive action (delete this meeting) is pushed to the far left instead:
   the same auto-margin trick every well-built dialog uses, so a thumb going for
   Save cannot land on Delete. */
.editor-actions {
  display: flex; align-items: center; justify-content: flex-end;
  flex-wrap: wrap; gap: 8px;
  padding-top: 14px; margin-top: 2px;
  border-top: 1px solid var(--line-soft);
}
.editor-actions > * { order: 1; }
.editor-actions .btn.primary { order: 2; }
.editor-actions .btn.danger,
.editor-actions .hint,
.editor-actions .perm-note { order: 0; margin-right: auto; }
.editor-actions .perm-note { margin-bottom: 0; }

/* ===========================================================================
 * THE AGENDA BUILDER
 *
 * Seven fields on a line, repeated for every session of a two-day retreat, is
 * where a form stops being readable. Three things keep it legible: the rows are
 * GROUPED BY DAY, so the date is answered once by a heading instead of being
 * re-read on every row; each row is a card of its own on a sunken ground, so
 * the eye can tell where one session ends; and the fields WRAP rather than
 * shrink, so nothing is ever narrower than the value it holds.
 *
 * FLEX-WRAP, NOT A GRID OF NAMED COLUMNS, AND THAT IS A DELIBERATE TRADE. A
 * grid would align the "session" boxes of every row down the page, which is
 * prettier; it would also need this file to know which child is which, and the
 * markup is written in another file by another hand. Wrapping degrades into a
 * labelled stack for ANY set of children, which is the failure this half of the
 * app can afford. The width hints below are hints: if the field order changes,
 * the row still lays out, it just shares the space differently.
 * ======================================================================== */
.agenda-builder { display: flex; flex-direction: column; gap: 12px; }
.agenda-day {
  border: 1px solid var(--line-soft); border-radius: var(--radius);
  background: var(--surface-sunken);
  padding: var(--pad-3);
}
/* The day's heading and its "add a session" button. app.js reuses .card-head
   here, which is the right element and the wrong padding: .card-head is built
   for the top of a card, so inside a group that already has 12px of its own it
   inset the date twice and drew a rule that stopped short of both edges. The
   padding is taken back and only the rule is kept - it is what separates the
   day from its sessions. */
.agenda-day > .card-head {
  padding: 0 0 var(--pad-2); margin-bottom: var(--pad-2);
  border-bottom: 1px solid var(--line);
}
/* Set as a section label rather than a title: the meeting's own name is already
   two headings up, and a third competing heading is how a form starts looking
   like a document. */
.agenda-day .card-head h3, .agenda-day .card-head h4,
.agenda-day > h3, .agenda-day > h4, .agenda-day > .day-label {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-muted);
}
.agenda-day > h3, .agenda-day > h4, .agenda-day > .day-label { margin: 0 0 8px; }
.agenda-day > .btn { margin-top: 8px; }
.agenda-day .empty { padding: var(--pad-1) 0 var(--pad-2); }
/* The "nothing on this day yet" line, which is a <p> rather than an .empty. */
.agenda-day > p { margin: 0; }

.agenda-row {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--pad-2) var(--pad-3);
  padding: var(--pad-2) var(--pad-3);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
.agenda-row + .agenda-row { margin-top: 8px; }
.agenda-row > * { flex: 1 1 8rem; min-width: 0; }

/* THE WIDTHS ARE KEYED TO WHAT A FIELD IS, NOT TO WHERE IT SITS. The obvious
   build is a ladder of :nth-child widths, and it was written that way first:
   the agenda row's field order then changed twice in an afternoon, and each
   time the ladder silently handed the session box the day picker's width.
   data-ag is the hook app.js already puts on the control it reads back, so it
   is exactly as stable as the field itself - and a row rearranged tomorrow
   keeps its proportions instead of quietly getting them wrong.
   Two things are being bought by the numbers. The bases are small enough that
   five fields AND the buttons hold one line on a laptop - measured against the
   narrowest real case, a 1440px screen minus the 15rem rail, the page's own
   1080 cap and four nested paddings, which leaves about 950px. And the GROWTH
   is uneven, so the slack all lands on detail, the field that holds a sentence
   while the others hold a word or a clock. time never grows at all: a wide box
   for "09:00" reads as a mistake. */
.agenda-row .field:has([data-ag="date"]) { flex: 0 1 9.5rem; }
.agenda-row .field:has([data-ag="time"]) { flex: 0 1 7rem; }
.agenda-row .field:has([data-ag="session"]) { flex: 1.3 1 10rem; }
.agenda-row .field:has([data-ag="detail"]) { flex: 2 1 12rem; }
.agenda-row .field:has([data-ag="lead"]) { flex: 1 1 8rem; }

.agenda-row .field { margin-bottom: 0; gap: 3px; }
.agenda-row .field > label { font-size: 10px; letter-spacing: .06em; }
/* Denser than the form above it, and written to work on a bare <input> as well
   as on one inside a .field - a builder row is the one place in this app where
   a label per cell is verbose enough that app.js may well leave it out. */
.agenda-row input, .agenda-row select, .agenda-row textarea {
  width: 100%; min-width: 0;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 7px 9px;
  /* The builder is the one place that is deliberately denser than the form
     above it, so it takes back the app-wide control height rather than
     inheriting a 43px box for a field holding "09:00". Still a thumb-sized
     36px, and the phone block below grows the buttons beside it to 38. */
  min-height: 2.25rem;
  background: var(--surface); color: var(--ink);
  font: inherit; font-size: 13.5px;
}
.agenda-row textarea { min-height: 38px; resize: vertical; }

/* THE BUTTONS MUST NOT MOVE WHEN THE ROWS DO. Up, down and remove sit in a slot
   of a fixed width, at a fixed size, aligned to the right - so pressing "down"
   four times is four presses in the SAME PLACE rather than a chase down the
   page. The first row's "up" and the last row's "down" do nothing, and the fix
   for that is a disabled button, never a missing one: removing it re-centres
   the other two and every button under the cursor becomes a different button.
   The [hidden] rule is insurance for exactly that - if app.js hides one, it
   still holds its ground. */
.agenda-row-actions {
  display: flex; justify-content: flex-end; align-items: center; gap: 4px;
  flex: 0 0 auto; min-width: 6.5rem;
  /* Right-hand end of the row whether or not the fields left room for it on the
     same line: without the auto margin a wrapped slot starts a new line at the
     LEFT, under the date, which is both the wrong place and a different place
     from the row above it. */
  margin-left: auto;
  align-self: flex-end; padding-bottom: 1px;
}
.agenda-row-actions .btn {
  flex: none;
  width: 30px; height: 30px; padding: 0;
  justify-content: center;
  font-size: 13px; line-height: 1;
}
.agenda-row-actions .btn[hidden] { display: inline-flex; visibility: hidden; }

/* ===========================================================================
 * MONEY
 *
 * A COLUMN OF FIGURES LINES UP ON THE DECIMAL POINT OR IT IS NOT A COLUMN.
 * Right-aligned and tabular-nums, both, always: proportional digits make 1,111
 * narrower than 8,888 and the column comes out ragged, which is exactly the
 * shape that hides an amount with a zero too many. The sign app.js prints sits
 * OUTSIDE the number ('− €640.00'), and '+' and U+2212 are cut to the same
 * width in every UI font this app ships, so the prefix does not break the
 * alignment either.
 *
 * IN AND OUT DIFFER BY FILL, WEIGHT AND HUE - IN THAT ORDER OF IMPORTANCE.
 * Money in is the marked case: a tinted ground, heavier, green. Money out is
 * plain ink and carries the minus it was given. That way the distinction is a
 * box against no box before it is ever a colour, it survives a greyscale print,
 * and - the point - a negative number is never legible only because it is red.
 * Expenses are also NOT painted in --danger: a dinner is not an error, and a
 * budget page where every line is red is the "coloured cards competing for
 * attention" this file was written against.
 *
 * NO PADDING IS ADDED HERE, EVER. The tint is drawn on whatever box already
 * exists - the cell if the class is on the <td>, the glyphs if it is on a
 * <span>. An inline pill with 6px of padding would inset the tinted rows by
 * 6px and un-align the very column this block exists to align.
 * ======================================================================== */
.money {
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
/* The column follows the cell whether app.js marks the <td> or a <span> inside
   it. :has is already load-bearing in this file (the shell and the top bar both
   depend on it), so this is not a new dependency. */
.table td.money, .table th.money,
.table td:has(> .money), .table th:has(> .money) { text-align: right; }

.money.in {
  background: var(--ok-soft); color: var(--ok); font-weight: 700;
  border-radius: 4px;
}
.money.out { color: var(--ink); font-weight: 600; }
/* Row hover paints every cell with --surface-sunken at a higher specificity
   than .money.in, and the tint - the one cue that is not colour - vanished
   under the pointer, on the rows a member was pointing at because they were
   reading them. */
.table tbody tr:hover td.money.in { background: var(--ok-soft); }

/* THE WORD AFTER THE NUMBER GETS A FIXED SLOT, OR IT UNDOES THE ALIGNMENT.
   app.js writes the direction out in words - "− € 3,880.00 out", "+ € 1,200.00
   in" - which is the right call: it is the cue that survives a greyscale print
   and a reader who cannot see the green. But it is set INSIDE the right-aligned
   cell, so "in" and "out" are different widths and every figure above an "in"
   sits three pixels further right than the one above it. A column that is
   nearly aligned is worse than one that is not, because the eye trusts it. One
   width for the word puts the decimal points back in a line. */
.money > .small, .money > .muted { display: inline-block; width: 2.2em; text-align: left; }

.money-total { font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }

/* A totals row belongs to the table, so it is drawn as its own band. The
   first-child rule that pins the leftmost column paints --surface over it, at a
   specificity a plain tfoot rule cannot reach - hence the second line. */
.table tfoot td { border-top: 1px solid var(--line); border-bottom: 0; background: var(--surface-sunken); font-weight: 700; }
.table tfoot td:first-child { background: var(--surface-sunken); }

/* ===========================================================================
 * WHAT A FIGURE IS WORTH IN THE READER'S OWN MONEY
 *
 * The forum keeps one set of books in one currency, and a member who lives in
 * euro can be shown what a franc line is worth to them. That figure sits
 * BESIDE the book figure and never instead of it - two members quoting the
 * same row to each other must be quoting the same number - so everything here
 * is about making it obviously the secondary one.
 *
 * SECONDARY MEANS SMALLER AND LIGHTER, NOT FAINTER THAN LEGIBLE. app.js sets
 * this span .small .faint, and .faint measured 3.1:1 on paper: a currency
 * conversion nobody can read is not a courtesy, it is a second number on a
 * money page that a reader has to lean in at. The token behind .faint is now
 * legible (see the palette), and this rule states --ink-muted anyway, because
 * a converted balance is the one place in the app where the third tier carries
 * a figure rather than a footnote. Never bold, never tabular-aligned into the
 * book column: it is an aside, and an aside that lines up is a second column.
 * BOTH NAMES ARE HERE ON PURPOSE. The class contract calls it .converted; the
 * client currently emits .alt-money. One rule, so the figure is right on
 * whichever side the rename lands.
 * ======================================================================== */
.converted, .alt-money {
  font-size: .85em;
  font-weight: 400;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Inside a right-aligned money cell the aside would otherwise be flung against
   the same edge as the figure it explains, and the two would read as one long
   number. A little air, and it stays on the figure's line. */
.money .converted, .money .alt-money { margin-left: var(--pad-1); }

/* ===========================================================================
 * THE PER-MEMBER BALANCES
 *
 * WHO IS OWED AND WHO IS OWING, WITHOUT COLOUR CARRYING IT.
 * Six members, some of them of an age where colour vision is not what it was,
 * and roughly one person in twelve cannot separate red from green at all. So
 * the direction is said THREE times: the sign app.js prints in front of the
 * number, the word it prints after it, and the fill .money.in/.money.out give
 * the figure. Read this list on a photocopier and it still answers the only
 * question anyone opens it to ask.
 *
 * THE FIGURES ALIGN ON THE DECIMAL OR THIS IS NOT A LEDGER. Right-aligned in
 * their own track, tabular-nums, and the word after the number gets a fixed
 * slot - "owes" and "owed" are different widths, and a column that is nearly
 * aligned is worse than one that is not, because the eye trusts it.
 * ======================================================================== */
/* THE SHAPE THE CLIENT ACTUALLY EMITS IS A .list-row, and a row of members'
   balances is a LEDGER COLUMN whatever the markup calls it. A flex row sizes
   each figure to its own content, so "− CHF 83.32" and "+ CHF 1,416.60" end at
   different x and the column comes out ragged - which is exactly the shape
   that hides an amount with a zero too many. A floor on the track, not a fixed
   width: a forum with a five-figure balance gets a wider column rather than a
   clipped one, and everything else in the app stays where it is because only a
   row that actually carries a figure is touched.
   THE TINT COMES OFF HERE, AND ONLY HERE. .money.in's green ground is the
   colourblind-safe cue in the transaction TABLE, where a row is six terse
   cells and there is nowhere to say it in words. This row says it in words -
   "They owe the forum this" - beside a sign and a heavier weight, so the fill
   is the fourth cue, not the first; and stretched across a fixed track it
   would paint a 9rem green block behind a 6rem number, which is the
   highlighter-left-switched-on that .total-card had to fix once already. */
.list-row:has(> .money) > .money {
  flex: 0 0 auto; min-width: 9rem;
  text-align: right;
}
.list-row > .money.in { background: none; }
/* THE CONVERTED FIGURE DROPS TO ITS OWN LINE RATHER THAN SHARING THE ONE THE
   COLUMN LIVES ON, and the reason is that only SOME rows have one: the client
   shows a member what a figure is worth in their money on their own row and on
   nobody else's. Left in the line it takes 124px out of that row and only that
   row, so the book figure on the reader's own line ends 124px short of the
   five below it - measured - and the one column on this page whose whole job
   is to be comparable is the one that is not.
   Under the figure, right-aligned, so it still reads as an aside about the
   number above it. The book figure stays last in the line and therefore
   flush with every other row's, and it stays FIRST in the reading order,
   which is the half of "beside it, never instead of it" that matters. */
.list-row:has(> .converted), .list-row:has(> .alt-money) { flex-wrap: wrap; }
.list-row > .converted, .list-row > .alt-money {
  flex: 1 0 100%;
  margin-top: 2px;
  text-align: right;
}

.balances { display: flex; flex-direction: column; }
.balance-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--pad-1) var(--pad-3);
  padding: var(--pad-2) 0;
  border-bottom: 1px solid var(--line-soft);
}
.balance-row:last-child { border-bottom: 0; }
.balance-row .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A member held in memoriam is still in the books, and the row says so rather
   than quietly reading like everybody else's. */
.balance-row.in-memoriam .name, .balance-row .in-memoriam { color: var(--ink-muted); }
.balance-row .money, .balances .money { justify-self: end; }
/* Wider than the 2.2em the transaction table's "in"/"out" needs: these words
   are "owes" and "owed" and they run to four and five characters. */
.balances .money > .small, .balances .money > .muted { width: 3.6em; }

/* ---- the splits on one transaction ----
 * A dinner divided six ways, shown under the row it belongs to. It has to be
 * legible and it has to be SMALL - it lives inside a table cell, and a split
 * list that doubles the height of every row turns the ledger into a stack of
 * cards nobody can scan. Two columns, name and figure, no borders: the rows
 * are a detail of the line above them, not a table of their own. */
/* A <tr> STAYS A TABLE ROW. The first version of this block set display:grid on
 * .split-row, which is the expander's <tr> - and a grid display takes an element
 * out of table layout, so colspan stopped applying and the whole row collapsed
 * to its content width. Measured: a 990px table with a 91.8px row hanging off
 * the left edge, every name wrapped onto two lines. The layout belongs on the
 * container INSIDE the cell, never on the row itself. */
.split-row > td { padding: var(--pad-1) var(--pad-2) var(--pad-2); }
.splits {
  display: grid;
  /* Wraps to as many columns as fit rather than one per member: six people at
     one per row is a stack taller than the entry it explains. */
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: 2px var(--pad-3);
  font-size: 12.5px;
}
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--pad-2);
  color: var(--ink-muted);
}
.split .who { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.split .money { font-size: inherit; font-weight: 600; color: var(--ink); }
/* The tint belongs to the ledger's own figures, not to a six-line detail under
   one of them: six green grounds inside one table row is the highlighter left
   switched on that the money block above was written against. */
.split .money.in { background: none; color: var(--ok); }

/* ---- the Wise feed ----
 * A READ-ONLY LIST OF THINGS THAT HAPPENED IN A BANK ACCOUNT, sitting beside a
 * ledger somebody keeps by hand. It is a proposal, never a fact about the
 * books, so it is drawn on the sunken ground rather than as another card of
 * white: this panel is evidence, and the ledger is the record.
 *
 * MATCHED IS QUIETER, NOT GREENER. A line already in the books needs no
 * action, so it recedes - lighter ink, the tint dropped from its figure - and
 * the row keeps whatever word app.js prints. Marking it by colour alone would
 * put the one piece of state on this panel into the one channel some of these
 * readers do not have.
 * ======================================================================== */
.wise-panel {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-sunken);
  padding: var(--pad-3) var(--pad-4);
}
.wise-panel > :last-child { margin-bottom: 0; }
.wise-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--pad-1) var(--pad-3);
  padding: var(--pad-2) 0;
  border-bottom: 1px solid var(--line-soft);
  font-size: 13.5px;
}
.wise-row:last-child { border-bottom: 0; }
.wise-row .date { font-variant-numeric: tabular-nums; color: var(--ink-muted); white-space: nowrap; }
.wise-row .desc { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wise-row .money { justify-self: end; }
.wise-row.matched { color: var(--ink-muted); }
.wise-row.matched .money.in { background: none; color: var(--ink-muted); font-weight: 600; }
.wise-row.matched .money.out { color: var(--ink-muted); }
/* The button that files a proposal into the books belongs at the right end of
   the row on the line below the figure, so a row that wraps does not put it
   somewhere different from the row above it. */
.wise-row .btn { justify-self: end; grid-column: 3; }
/* ---- the connection, above the feed ----
 * WHAT IS TRUE ABOUT THIS CONNECTION, AS A CHECKLIST RATHER THAN A SENTENCE.
 * Which account, live or sandbox, whether statements will work, who set it up:
 * four facts that get scanned for one at a time, so they are four items with a
 * dot each rather than a paragraph somebody has to read to the end of.
 */
.wise-facts {
  list-style: none;
  margin: var(--pad-2) 0 var(--pad-3);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--pad-1) var(--pad-4);
}
.wise-facts li { display: flex; align-items: baseline; gap: 7px; }
/* The dot is decoration and is marked as such: a screen reader announcing
   "bullet" four times adds nothing that the list structure has not said. */
.wise-facts li::before {
  content: "";
  flex: none;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--ink-faint);
}

/* The form is a second act inside the same card - connect, rather than look at
   what is connected - so it is separated by a rule instead of being another
   white box on white. */
.wise-form { border-top: 1px solid var(--line); margin-top: var(--pad-3); padding-top: var(--pad-3); }
.wise-form > :first-child { margin-top: 0; }
/* A PEM key is 1,700 characters of base64 that must survive a paste unbroken.
   Monospace so a wrapped line is still readable as the key it is, and
   nowrap-free so nothing about the wrapping changes what gets submitted. */
.wise-form textarea { font-family: var(--mono); font-size: 12px; line-height: 1.5; }


/* ===========================================================================
 * THE OVERALL: TOTAL CARDS AND THE BREAKDOWN
 *
 * "Overall first, then per transaction" is the page, so the top of it has to
 * answer in one look: in, out, balance. Three or four cards, the figure big and
 * tabular so the three can be compared vertically, the label small above it -
 * a number that large with no label is a number nobody trusts.
 *
 * THE BAR IS SIZED BY A CUSTOM PROPERTY BECAUSE THE CSP FORBIDS THE OBVIOUS.
 * style-src 'self' with no 'unsafe-inline' means a width cannot be written into
 * the markup - the emotion wheel learned this the expensive way, rendering as
 * grey tiles with no error at all. element.style.setProperty() is a DOM call
 * rather than a style attribute in generated HTML, so it is allowed, and
 * --pct is the one value app.js sets.
 *
 * --pct IS A UNITLESS NUMBER FROM 0 TO 100. Not '42%' - `calc(var(--pct) * 1%)`
 * makes the percentage, and a value that arrives already carrying a unit is
 * rejected by the @property registration below and falls back to 0. That is a
 * bar at zero rather than a bar of unpredictable width, which is the failure
 * worth having: an empty bar is visibly wrong, a wrong bar is not.
 * ======================================================================== */
/* INHERITS, AND IT HAS TO. The fill is a ::before of the bar, and a custom
   property registered as inherits:false is NOT passed to a pseudo-element - it
   resolves to the initial value there instead. Registered the tidy-looking way,
   every bar sat at --pct:47 and painted a fill of exactly zero pixels, with no
   error anywhere: the same class of silent refusal the emotion wheel hit. */
@property --pct {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

.totals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: 12px;
  margin: 0 0 20px;
}
.total-card {
  display: flex; flex-direction: column; gap: 3px;
  /* flex-start, not the default stretch, and the reason is the money tint: a
     stretched .figure is as wide as the card, so .money.in painted its ground
     across four inches of empty card and read as a highlighter left switched
     on. The mark has to hug the number it marks. */
  align-items: flex-start;
  min-width: 0;
  /* A card, so a card's inset. It was 13px 15px - one pixel off the ladder on
     both axes, which is invisible on its own and reads as three boxes that do
     not quite line up when the three sit in a row across the top of the budget
     page beside a .card that is inset 16. */
  padding: var(--pad-3) var(--pad-4);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
}
/* Label above figure however app.js orders them: one card in a row of four with
   its label underneath makes the whole row look misaligned. EVERYTHING ELSE IS
   ORDERED TOO, and that is not belt-and-braces - a card also carries a sentence
   under the figure, and ordering only two of three children left the third at
   the default 0, which put the explanation ABOVE the label it explains. */
.total-card > * { order: 2; }
.total-card .label { order: 1; }
.total-card .label, .breakdown-row .label {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-muted);
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.total-card .figure, .breakdown-row .figure {
  font-size: 25px; font-weight: 700; line-height: 1.15; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
/* A figure that is also .money would otherwise be flung to the right edge of
   its own card, away from the label it belongs to. In a card the figures are
   compared DOWN the column of cards, and they align on the left. */
.total-card .money, .total-card .figure { text-align: left; }
.total-card .small, .total-card .muted { font-size: 12.5px; }

.breakdown { display: flex; flex-direction: column; gap: 9px; }
/* Placed by class rather than by source order, so the category, the bar and the
   amount land in the same three columns whatever order the markup lists them
   in - and every row's bar therefore starts and ends at the same x, which is
   the only reason a stack of bars can be compared at all. */
.breakdown-row {
  display: grid;
  grid-template-columns: minmax(6rem, 13rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: 4px 12px;
}
.breakdown-row > .label { grid-column: 1; }
.breakdown-row > .breakdown-bar { grid-column: 2; }
.breakdown-row > .money, .breakdown-row > .money-total { grid-column: 3; }

.breakdown-bar {
  --pct: 0;
  position: relative;
  height: 8px; min-width: 2.5rem;
  border-radius: 999px;
  background: var(--surface-sunken);
  overflow: hidden;
}
/* clamp() rather than a bare calc, because a proportion of a total is arithmetic
   done elsewhere and one rounding error away from 100.4 - which would paint a
   fill wider than the track it sits in. */
.breakdown-bar::before {
  content: "";
  position: absolute; top: 0; bottom: 0; left: 0;
  width: clamp(0%, calc(var(--pct) * 1%), 100%);
  border-radius: inherit;
  background: var(--accent);
}

/* AND THE SHIPPING BAR IS A <progress>, WHICH IS BETTER THAN THE DIV ABOVE.
 * app.js reached the same conclusion about the CSP from the other side and
 * picked the element whose length is set by ATTRIBUTES - value and max - so no
 * custom property is needed at all, and a screen reader is told what the length
 * MEANS rather than being handed a decorative box. This file dresses it and
 * keeps the div path above for anything that sets --pct instead; a <progress>
 * never runs that rule, because a replaced element grows no ::before.
 *
 * THE UA BAR MUST BE TURNED OFF IN THREE PLACES OR IT IS WRONG IN THE DARK.
 * Left alone this is a platform widget: a beige trough on macOS, a green fill
 * on Android, and neither of them has heard of --surface-sunken - so the one
 * graphic on the budget page would be the one thing that ignores the theme.
 * Each engine exposes the trough and the fill under its own pseudo-element and
 * they cannot be combined into one selector: an unrecognised pseudo-element
 * invalidates the whole rule, so a WebKit name and a Firefox name in the same
 * list means neither browser applies it. Hence three rules that say one thing.
 * The bare `color` is for the engines that paint the fill with it.
 */
progress.breakdown-bar {
  -webkit-appearance: none; appearance: none;
  display: block; width: 100%;
  border: 0;
  background: var(--surface-sunken);
  color: var(--accent);
}
progress.breakdown-bar::-webkit-progress-bar { background: var(--surface-sunken); border-radius: 999px; }
progress.breakdown-bar::-webkit-progress-value { background: var(--accent); border-radius: 999px; }
progress.breakdown-bar::-moz-progress-bar { background: var(--accent); border-radius: 999px; }

/* ===========================================================================
 * THE QUIET DAY
 *
 * Most days there is nothing to do in this app. Somebody opens it anyway, and
 * what they get is a line worth reading and the one fact they came for: when
 * do I next see everyone. Both have to look like INFORMATION. A quiet day is
 * the normal state of a forum between meetings, and a screen that dresses it
 * up as an empty state is a screen that says something is missing.
 *
 * A QUOTE IS SET LARGE, NOT ITALIC AND CENTRED IN A BOX. The italic-in-a-box
 * pull-quote is a magazine device for text the reader is already reading;
 * here the quote IS the page, and the treatment it wants is the one a book
 * gives an epigraph - a step up in size, ordinary roman, ranged left, ending
 * where the measure ends. Two things have to be undone for that: a
 * <blockquote> carries a 40px UA indent (measured - the quote started 40px
 * right of the sentence underneath it, which reads as a rendering fault) and a
 * <cite> is italic by UA.
 * ======================================================================== */
.quote, .day-quote {
  margin: 0;
  max-width: var(--measure);
  font-size: 18px; line-height: 1.5; font-style: normal;
  color: var(--ink);
  letter-spacing: -.005em;
}
.quote p, .day-quote p { margin: 0; max-width: none; font-size: inherit; line-height: inherit; }
/* Attribution, not a second sentence: it sits under the line, smaller, quieter
   and roman. The em dash belongs to the markup if it wants one. */
.quote-who, .quote cite, .day-quote cite {
  display: block;
  margin-top: var(--pad-2);
  font-size: 13px; font-style: normal; font-weight: 600;
  color: var(--ink-muted);
}

/* INFORMATION, NOT AN ALERT. The next meeting is a fact, and this app has a
   whole vocabulary for things that need attention - the banner's tinted panel,
   its 3px bar, --warn - which this line gets none of. It is a sentence at the
   body's own size with the name of the meeting in it, one step up from the
   prose around it because it is the answer, and that is all. */
.next-meeting, .next-line {
  max-width: var(--measure);
  font-size: 15px; line-height: 1.6;
  color: var(--ink);
}
.next-meeting strong, .next-line strong { font-weight: 700; }

/* ===========================================================================
 * THE PHONE, FOR EVERYTHING ABOVE
 * The rule the whole file is built around still holds: the page never scrolls
 * sideways. The editor's pairs become a stack, the builder's rows become
 * labelled fields, the totals become one column, and the breakdown puts its bar
 * on its own line under the name and the amount rather than squeezing three
 * things into 340px.
 * ======================================================================== */
@media (max-width: 760px) {
  .editor-row { grid-template-columns: minmax(0, 1fr); }
  /* Every field takes the full width: at this size a two-up row leaves each
     control about 150px, which is narrower than the date it has to display. */
  .agenda-row > *,
  .agenda-row .field:has([data-ag]) { flex: 1 1 100%; }
  .agenda-row { padding: var(--pad-3); }
  .agenda-row-actions {
    flex: 1 1 100%; width: 100%;
    margin-top: 2px; padding-top: var(--pad-2);
    border-top: 1px solid var(--line-soft);
  }
  /* Thumb-sized, because these three are now the only controls on the line. */
  .agenda-row-actions .btn { width: 38px; height: 38px; }
  .totals { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .total-card .figure, .breakdown-row .figure { font-size: 22px; }
  /* Save goes full width on its own line at the bottom of the footer, where a
     thumb reaches it without crossing Cancel or Delete. */
  .editor-actions .btn.primary { flex: 1 1 100%; justify-content: center; }
}

/* The bar goes under the pair rather than between them. The ROW is named as
   well as the column: an item that spans both columns cannot share a row with
   one that has not been placed, so auto-placement pushed the amount down onto a
   third line and the category, its bar and its figure read as three things. */
@media (max-width: 560px) {
  .breakdown-row { grid-template-columns: minmax(0, 1fr) auto; gap: 5px 10px; }
  .breakdown-row > .label { grid-column: 1; grid-row: 1; }
  .breakdown-row > .money, .breakdown-row > .money-total { grid-column: 2; grid-row: 1; }
  .breakdown-row > .breakdown-bar { grid-column: 1 / -1; grid-row: 2; }
}

/* ---------------------------------------------------------------------------
 * The account strip, and the menu behind a member's own face.
 *
 * WHY THE FACE IS THE CONTROL
 * Top right is where a person looks for themselves. The four things members
 * change most often - light or dark, their picture, the address their sign-in
 * links go to, and which version this is - all live behind it, and a face is
 * recognisable at a glance in a way a gear icon is not.
 * ------------------------------------------------------------------------- */
.column { display: flex; flex-direction: column; min-width: 0; min-height: 100vh; }
.topbar {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: var(--pad-2, 10px) var(--pad-3, 18px);
  border-bottom: 1px solid var(--line);
  background: var(--bg);
  /* Sticky rather than fixed: fixed would need the main column padded to match
   * and the two would drift apart the first time this bar changed height. */
  position: sticky;
  top: 0;
  z-index: 30;
}
.topbar[hidden] { display: none; }
.topbar-in { position: relative; }

.account {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 4px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: none;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.account:hover { background: var(--surface-sunken, var(--surface)); }
.account:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.account[aria-expanded="true"] { background: var(--surface-sunken, var(--surface)); border-color: var(--line); }
.account-name { font-size: 14px; font-weight: 600; }
.account-caret { display: inline-flex; color: var(--ink-muted); }
.account-caret svg { width: 16px; height: 16px; }

/* The face, or the initials drawn in its place. Square, round, and never
 * distorted: object-fit stops a portrait photo being squeezed into a circle. */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  overflow: hidden;
  flex: none;
  object-fit: cover;
  background: var(--surface-sunken, var(--surface));
}
.avatar-32 { width: 32px; height: 32px; font-size: 12px; }
.avatar-40 { width: 40px; height: 40px; font-size: 14px; }
.avatar.initials { font-weight: 700; letter-spacing: .02em; color: #14202c; }
/* Six tones, keyed off the member id so a rename does not change somebody's
 * colour. Dark text on all six in both themes, which is why they stay pale. */
.avatar.tone-1 { background: #f3e07f; }
.avatar.tone-2 { background: #a8c6e8; }
.avatar.tone-3 { background: #9fd6b4; }
.avatar.tone-4 { background: #f2a6a0; }
.avatar.tone-5 { background: #c9b6e4; }
.avatar.tone-6 { background: #f5d089; }

.menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  width: min(21rem, calc(100vw - 24px));
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius, 10px);
  background: var(--surface);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .18);
  z-index: 40;
  max-height: min(32rem, calc(100vh - 5rem));
  overflow: auto;
}
.menu-head { display: flex; align-items: center; gap: 10px; padding: 10px; }
.menu-who { display: flex; flex-direction: column; min-width: 0; }
.menu-who strong { font-size: 14px; }
.menu-who span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-group { border-top: 1px solid var(--line-soft, var(--line)); padding: 6px 0; }
.menu-label {
  padding: 4px 10px;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-muted);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: var(--radius-sm, 7px);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.menu-item:hover { background: var(--surface-sunken, var(--bg)); }
.menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.menu-item svg { width: 18px; height: 18px; color: var(--ink-muted); flex: none; }

/* The theme switch. A segmented control rather than a select, because there are
 * three options and all three fit: a member should be able to see what they are
 * choosing between without opening anything. */
.seg { display: flex; gap: 4px; padding: 4px 10px 8px; }
.seg-item {
  flex: 1;
  padding: 7px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm, 7px);
  background: var(--bg);
  color: var(--ink-muted);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
}
.seg-item:hover { color: var(--ink); }
.seg-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The current one is marked by weight and border as well as fill, so it is
 * still obvious in greyscale and to anybody who does not see the accent. */
.seg-item.on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink, #fff);
  font-weight: 700;
}

.menu-foot { border-top: 1px solid var(--line-soft, var(--line)); padding: 4px 0 2px; }
.menu-version {
  display: block; width: 100%;
  padding: 8px 10px;
  border: 0; background: none; color: var(--ink-muted);
  font: inherit; font-size: 12.5px; text-align: left; cursor: pointer;
}
.menu-version:hover { color: var(--ink); }
.changelog { padding: 0 10px 8px; }
.changelog section { padding: 8px 0; border-top: 1px dashed var(--line-soft, var(--line)); }
.changelog h4 { margin: 0 0 4px; font-size: 13px; }
.changelog p { margin: 0 0 6px; font-size: 12.5px; line-height: 1.5; color: var(--ink-muted); }
.changelog strong { color: var(--ink); }

@media (max-width: 700px) {
  /* The name is the first thing to go: the face still identifies the control,
   * and the row has a Menu button on the other end of it. */
  .account-name { display: none; }
}
