/* =============================================================================
   Members - admin stylesheet

   Plain CSS, no build step and no framework, in the same spirit as the
   common.css in the neighbouring projects: one file, uploaded as it is.  It is
   served from /assets/css/app.css by nginx (or by the PHP dev server for
   /router.php).

   Layout is single-column with a two-column grid that collapses under 760px, so
   the member list and forms work on a phone - which is where a membership
   secretary actually updates a record after a club night.
   ========================================================================== */

:root {
  --bg: #f4f6f8;
  --surface: #ffffff;
  --ink: #1b2027;
  --ink-soft: #4b5563;
  --muted: #6b7280;
  --line: #e2e6ea;
  --line-strong: #cbd2d9;
  --accent: #1f5fd0;
  --accent-dark: #17479b;
  --good: #1a7f4b;
  --good-bg: #e7f6ec;
  --warn: #8a5a00;
  --warn-bg: #fdf3dd;
  --bad: #b3261e;
  --bad-bg: #fdecea;
  --info-bg: #e8f0fe;
  --radius: 10px;
  --radius-sm: 6px;
  --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .1);
}

/* Dark mode.
 *
 * The palette is one set of variables, so this is a second set rather than a second stylesheet -
 * and anything added later inherits it by using a variable, which is the whole reason the colours
 * were variables to begin with.  The surfaces are the same slate as the top bar, so the header does
 * not change colour when the theme does and the page does not look like two designs stapled
 * together.
 *
 * Set on <html> by assets/js/app.js, from a preference kept per account in localStorage. */
[data-theme="dark"] {
  /* Tells the browser itself, so scrollbars, form controls and the canvas behind the page are dark
     too.  Without it a dark page keeps a white scrollbar track down the side. */
  color-scheme: dark;
  --bg: #151a22;
  --surface: #1e242f;
  --ink: #e8ecf1;
  --ink-soft: #c3cbd6;
  --muted: #98a3b3;
  --line: #2c3441;
  --line-strong: #3a4353;
  --accent: #6f9cf5;
  --accent-dark: #4a7ce0;
  --good: #5cc98d;
  --good-bg: #16301f;
  --warn: #e0b45e;
  --warn-bg: #33280f;
  --bad: #f0908a;
  --bad-bg: #3a1c1a;
  --info-bg: #1c2b45;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 1px 3px rgba(0, 0, 0, .3);
}

/* The theme switch in the header.
 *
 * Both icons are always in the markup and the stylesheet chooses between them, so the button looks
 * right before any script has run and with scripting off entirely.  The script only changes which
 * one that is. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, .28);
  border-radius: var(--radius-sm);
  color: inherit;
  cursor: pointer;
}

.theme-toggle:hover { border-color: rgba(255, 255, 255, .55); }

.theme-toggle svg {
  width: 1.05rem;
  height: 1.05rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* One icon at a time, and the icon shows what the button *does* rather than what is on: a moon while
 * the page is light, offering dark, and a sun while it is dark.  The title attribute has always said
 * that - "Switch to dark mode" - and the icons said the opposite, which is why they were swapped. */
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
[data-theme="dark"] .theme-toggle .icon-moon { display: none; }

/* The handful of rules that name a colour instead of reading a variable.
 *
 * Everything else follows the theme by itself; these set a literal white and would stay light on a
 * dark page, which is exactly what a filter dropdown and the quiet buttons did - visible the moment
 * anybody looked at the page rather than at the test results.  Kept together, and named rather than
 * converted to variables, so the exceptions are all in one place and the next person can see them.
 *
 * The print stylesheet's own white body is deliberately not here: paper is white. */

[data-theme="dark"] .btn { background: #1b2028; border-color: var(--line-strong); color: var(--ink); }
/* :not(.btn-primary), because this rule sets only a background and a primary button's text is dark.
   Left to apply, it put the grey hover behind #10151c text and the label vanished - which is what
   "the text becomes unreadable on hover" was.  A rule that changes one half of a pair has to say
   which buttons it is for. */
[data-theme="dark"] .btn:hover:not(.btn-primary) { background: #232a33; }
[data-theme="dark"] .btn-primary { background: var(--accent); border-color: var(--accent); color: #10151c; }
/* The colour is repeated rather than inherited: a hover rule that names a background has to name the
   text that is readable on it, or it is one cascade accident away from the same bug. */
[data-theme="dark"] .btn-primary:hover { background: var(--accent-dark); border-color: var(--accent-dark); color: #10151c; }
[data-theme="dark"] .btn-danger { background: #1b2028; border-color: #7a3b33; color: var(--bad); }
[data-theme="dark"] .notes { background: #191e25; }

/* The rest of the literal lights - which is what "the background turns white" meant.  A table head,
 * a hovered row, the action chip in the activity list, a hovered button and the resigned pill all
 * named a near-white rather than reading a variable, so they stayed light on a dark page.
 *
 * Dark equivalents rather than flattening them to the page colour: the difference between a row and
 * its neighbour is what makes a long table readable, and a table where everything is one flat dark
 * is harder to scan than the light one it replaced. */
[data-theme="dark"] .table thead th { background: #1b2028; color: var(--ink-soft); }
[data-theme="dark"] .table tbody tr:hover { background: #191e25; }
[data-theme="dark"] code.action { background: #232a33; color: var(--ink-soft); }
/* The hovered button is set once, above, with the rest of the .btn rules.  A second copy lived here
   and did nothing but invite the two to drift apart. */
/* .pill and .pill-resigned read --line now rather than a literal grey, so they follow the theme
 * without help - which is what the OFFERED column's "No" needed.  Only the colour of the text is
 * still worth saying here. */
[data-theme="dark"] .pill-resigned { color: var(--muted); }

/* The sign-in page and the like, which have no header and so no switch - but the cookie is theirs
 * too, so somebody who chose dark does not get a white page the moment they sign out. */
[data-theme="dark"] .body-bare { background: var(--bg); }

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

h1 { font-size: 1.5rem; margin: 0 0 .25rem; letter-spacing: -.01em; }
h2 { font-size: 1.05rem; margin: 0 0 .75rem; }
h3 { font-size: .95rem; margin: 1rem 0 .35rem; }

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

.muted { color: var(--muted); }
.small { font-size: .85em; }
.strong { font-weight: 600; }
.block { display: block; }
.nowrap { white-space: nowrap; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.text-bad { color: var(--bad); }
.lead { color: var(--ink-soft); margin: 0; }

/* ---------------------------------------------------------------- top bar -- */

.topbar {
  background: #1d2430;
  color: #eef1f5;
  position: sticky;
  top: 0;
  z-index: 20;
}

.topbar-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: .6rem 1rem;
  display: flex;
  align-items: center;
  /* One line, and tighter than it was.  The theme switch added about forty pixels and pushed the
     account area onto a second row at this width - which looked like a mistake because it was one.
     The header is meant to become two rows on a narrow screen, and that is restored in the media
     query at the foot of this file rather than by wrapping here at every size. */
  gap: 1rem;
  flex-wrap: nowrap;
}

/* And nothing in the bar shrinks.  Without this the long items give way instead of the bar wrapping
 * - the club's name and "Sign out" each broke onto two lines inside the header, which is worse than
 * the second row this was meant to avoid.  Below 900px it wraps again, which is the layout that
 * width was designed for. */
.topbar-inner > * { flex: 0 0 auto; }

/* The menu is the one exception: it holds a list of links that can wrap, and it has to be allowed
 * to take the width available rather than its full width.  Without this the bar is laid out at the
 * menu's width - 683px for a member's six items - and a phone narrower than that scrolls sideways,
 * which is what the measurement found at every width from 320 to 414.  `min-width: 0` is the part
 * that matters: a flex item will not shrink below its content's width without it.  The links wrap
 * into rows instead, which is what .nav's own flex-wrap was always there for. */
.topbar-inner > .nav { flex: 1 1 auto; min-width: 0; }

/* The club, with who is signed in beneath it, and the logo spanning both: the logo is a
   cell of its own so the club's name and the person's name start at the same place, and
   the person sits directly under the club rather than under the logo.  The name heads the
   page and the person reading it belongs with it, rather than on the far side of the
   menu - which also gives the menu the end of the bar to itself.

   `min-width: 0` keeps a long club name from setting the bar's width on a phone. */
.brand-block {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: .55rem;
  row-gap: .1rem;
  min-width: 0;
}

/* No logo: one column, so the name and the person do not start at an empty gap. */
.brand-block-plain { grid-template-columns: 1fr; }

.brand-mark { display: block; grid-column: 1; grid-row: 1 / span 2; }
.brand { grid-column: 2; grid-row: 1; }
.brand-name { line-height: 1.2; }
.brand-block-plain .brand { grid-column: 1; }

.brand-who {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  align-items: baseline;
  gap: .4rem;
  font-size: .78rem;
  line-height: 1.25;
  color: #9aa5b4;
}

.brand-block-plain .brand-who { grid-column: 1; }

/* The person, then quietly what they are.  Both are one line: a name that wrapped would
   push the menu about on a narrow bar. */
.brand-who .who { color: #eef1f5; font-weight: 600; white-space: nowrap; }
.brand-who .role { color: #9aa5b4; white-space: nowrap; }

.brand { font-weight: 700; color: #fff; letter-spacing: .01em; }
.brand:hover { text-decoration: none; }

/* The club's own logo.  Height rather than width, so a wide logo and a square one both
   fit the cell without either of them setting its width. */
.brand-logo {
  height: 28px;
  width: auto;
  display: block;
  border-radius: 4px;
}

.brand-name { line-height: 1.2; }

/* Above the card on the sign-in pages, in place of nothing at all when a club has
   not set a logo.  Only the signed-out pages use this class; the app's own header
   keeps its 28px one.

   A height, not a max-height.  An image renders at its own size, so a max-height can
   only ever make it smaller - which is why raising it stopped doing anything at all
   once it passed the file's own height.  This scales, and 120px is about 1.5x the
   80px logo the club has: beyond that it gets soft, and a bigger source file is the
   real answer. */
.bare-logo {
  display: block;
  margin: 0 auto .75rem;
  height: 120px;
  width: auto;
  max-width: 90%;
}

.nav { display: flex; gap: .25rem; flex-wrap: wrap; align-items: center; }

.nav a {
  color: #c7ced8;
  padding: .35rem .7rem;
  border-radius: var(--radius-sm);
}

.nav a:hover { background: rgba(255, 255, 255, .08); color: #fff; text-decoration: none; }
.nav a.is-active { background: rgba(255, 255, 255, .14); color: #fff; font-weight: 600; }

/* The page's actions: the switch, a password to change, and the way out.  The bar's last
   item, pushed to the right, so on a desk it ends the single row.  See the 900px
   breakpoint for where it goes once the bar has to wrap. */
.nav-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: .75rem;
  font-size: .9rem;
}

.link-quiet { color: #c7ced8; }
.link-quiet:hover { color: #fff; }

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

.wrap {
  max-width: 1180px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 3rem;
  /* An email address or a note with no spaces in it must wrap rather than push the
     whole page sideways: a page that scrolls horizontally is the first thing that
     makes a site look broken on a phone. */
  overflow-wrap: break-word;
}

.page-head {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}

.page-head-actions { margin-left: auto; display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }

.footer {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 1.25rem 2rem;
  color: var(--muted);
  font-size: .82rem;
  display: flex;
  gap: .35rem 1rem;
  justify-content: space-between;
  flex-wrap: wrap;
}

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

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.25rem;
  margin-bottom: 1.25rem;
}

.card-flush { padding: 0; overflow: hidden; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.card-narrow { max-width: 34rem; }
/* 2vh, not 8vh: the logo sits directly above this card, so the card's top margin is
   the empty band between the two.  The logo keeps a small gap of its own. */
.card-login { max-width: 25rem; margin: 2vh auto 2rem; padding: 1.75rem; }
.form-narrow { max-width: 34rem; }

.grid-2 {
  display: grid;
  /* `min(300px, 100%)` rather than a flat 300px: on a 320px phone the space inside a
     card is about 256px, and a 300px minimum made the columns wider than the screen -
     a horizontal scrollbar under every form.  Below 300px the track is the container's
     width, so the columns stack instead of overflowing. */
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 1.25rem;
  align-items: start;
}

.grid-2 > .card { margin-bottom: 1.25rem; }

/*
 * The member page.  Details is the tall panel in the left-hand column and runs down
 * two rows, so its content continues underneath the panels beside it: who the member
 * is on the left, and what hangs off the record to the right of them.
 *
 * Three explicit columns rather than .grid-2's auto-fit.  This page has a panel that
 * spans two rows, and a column count chosen by the width would shuffle the panels
 * around it from screen to screen - the point of the arrangement is that it does not
 * move.  Under 900px it falls back to two columns, and under 560px to one.
 */
.grid-member {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* One gap, doing both jobs.  A card with a margin of its own doubles the distance between
     rows while leaving the columns alone, which is what made the space look uneven. */
  gap: 1rem;
  align-items: stretch;
}

/* No margin of its own: the grid's gap is the only spacing here, so rows and columns match. */
.grid-member > .card { margin-bottom: 0; }

.grid-member > .card-tall { grid-row: span 2; }

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

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: .75rem;
  margin-bottom: 1.25rem;
}

.tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 4px solid var(--line-strong);
  border-radius: var(--radius);
  padding: .85rem 1rem;
  color: var(--ink);
  box-shadow: var(--shadow);
}

.tile:hover { text-decoration: none; border-color: var(--accent); }
.tile-value { display: block; font-size: 1.6rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.tile-label { display: block; color: var(--muted); font-size: .85rem; }
.tile-good { border-left-color: var(--good); }
.tile-warn { border-left-color: #d29a1a; }
.tile-bad { border-left-color: var(--bad); }
.tile-quiet { border-left-color: var(--muted); }

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

.table { width: 100%; border-collapse: collapse; }

.table th, .table td {
  text-align: left;
  padding: .6rem .9rem;
  border-bottom: 1px solid var(--line);
  /* Middle, not top: a cell holding a button otherwise hangs its text at the top of
     a row that the button has made taller. */
  vertical-align: middle;
}

/* An email address has no spaces to break at, so it sets the column's minimum width and
   pushed the last column off the edge of a phone. Only a link breaks mid-word; a name or a
   number stays exactly as it was typed. (`anywhere` is the value that actually lowers the
   intrinsic width - `break-word` leaves it alone - and it only breaks a word that does not
   fit, so nothing changes on a desk-sized screen.) */
.table td a { overflow-wrap: anywhere; }

.table thead th {
  background: #fafbfc;
  font-size: .78rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  white-space: nowrap;
}

.table thead th a { color: var(--muted); }
.table thead th a:hover { color: var(--accent); }
.table tbody tr:hover { background: #fafbfc; }
.table tbody tr:last-child td { border-bottom: 0; }
.row-deleted { opacity: .6; }
.actions { white-space: nowrap; }

/* The audit trail is one column of prose beside five that hold a single short value, and
   the prose is the only one worth reading. Left alone, `table-layout: auto` gives every
   column a share of the slack: "Record", holding nothing but "user 2", came out as wide as
   the sentence in "Detail", and the one column anybody reads was the narrowest of them.

   `.col-fit` is not enough here. It sizes a column to its content, but a table that must
   fill its container has to put the leftover *somewhere*, and it spreads it over the
   columns that are not tightly constrained - so Record still came out at 105px when its
   content needed 55. `table-layout: fixed` is what actually binds a width.

   Under `fixed` every width must be declared or the leftover is shared out evenly again,
   and the columns that are not constrained get clipped. So the five short columns are all
   sized here, from what their content measured at 1400px, and Detail is left unsized: it
   takes everything that remains, which is the point. Record and IP are the two that vary
   with the row - "user 2" against "member 31" - so they are sized for the longer of the
   two rather than the average.

   These numbers came from rendering the real table in Chrome and measuring it, not from
   reasoning about which `width` beats which table algorithm. That is worth repeating if
   they are ever revisited: `tests/_scratch/try_layouts.mjs` measures candidates at several
   viewport widths, and `verify_audit.mjs` re-checks the committed one. A solution that only
   works at one width is not a solution. */
.table-audit { table-layout: fixed; }
.table-audit th, .table-audit td { white-space: nowrap; }
.table-audit td.detail { white-space: normal; word-break: break-word; }

/* A percentage shrinks as the table narrows while the text in it does not, which is what
   wrapped "member 11" and then pushed "103.226.32.202" past the right edge. So the columns
   whose content is a value rather than prose are all sized in ch, absolute to the text, and
   the one column that is prose takes what is left. The ch figures are the longest value each
   column can hold plus a character of slack, measured in the browser rather than guessed. */
.table-audit th:nth-child(1), .table-audit td:nth-child(1) { width: 23ch; }  /* when   - 15 Oct 2026, 22:08:27 */
.table-audit th:nth-child(2), .table-audit td:nth-child(2) { width: 20ch; }  /* who    - a name on one line */
.table-audit th:nth-child(3), .table-audit td:nth-child(3) { width: 26ch; }  /* action - user.password_changed */
.table-audit th:nth-child(4), .table-audit td:nth-child(4) { width: 14ch; }  /* record - "member 31" */
.table-audit th:nth-child(6), .table-audit td:nth-child(6) { width: 16ch; }  /* ip     - 103.226.32.202 */
                                                                             /* detail - the rest */

/* Action buttons sit against the right edge of the row, instead of starting
   wherever the widest column happens to end and leaving a gap after them. */
.table th.actions, .table td.actions { text-align: right; }
.actions .btn { margin-left: .4rem; }
.actions > .btn:first-child, .actions > :first-child .btn { margin-left: 0; }

/* Columns that take only the width their content needs, so the leftover space goes
   to the text that is actually long rather than being shared out between a plate
   and a year. */
.table .col-fit { width: 1%; white-space: nowrap; }

/* The same, except that the heading above may wrap onto two lines. The column then
   matches the value - a date - instead of being stretched to fit the words in the
   heading, which is most of its width otherwise. Sized by its content rather than a
   fixed width, so it is as wide as the longest date in it and no wider. */
/* The date field and its Save button, side by side, in a column that is otherwise a date.  The
   field is not the full width of the cell: the general rule below makes every input fill its
   container, which in this cell left no room for the button beside it. */
.table .col-date { width: 11%; }
.table th.col-date { white-space: normal; }
.table td.col-date { white-space: nowrap; }
.table td.col-date input[type="date"] { width: auto; }

/* The vehicle's own column in the office list: it gives up the room the safety check needs.
   Not col-wide, which nine other tables use for a member's or a name's column. */
.table .col-vehicle { width: 18%; }

.table .col-wide { width: 34%; }
.table .col-notes { width: 22%; white-space: normal; }

/* A vehicle's odds and ends in one cell - here, what the club recorded and what the
   member typed - so it wraps rather than being clipped. */
.table .col-details { width: 24%; white-space: normal; }

/* The member's own vehicle list: the name, the year, then one Details column holding the
   last safety check and the member's notes, and the two buttons.

   The vehicle's name had a third of the row to itself, which left the rest of the row
   bunched against the right-hand edge with a gap in front of it. Narrower here, and the
   width it gave up is where the details went. */
.table-vehicles .col-wide { width: 24%; }
.table-vehicles .col-year { width: 8%; white-space: nowrap; }
.table-vehicles .col-details { width: 40%; }
.table-vehicles .col-actions { width: 1%; white-space: nowrap; }

code.action { color: var(--ink-soft); background: #f3f5f7; padding: .1rem .35rem; border-radius: 4px; }

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

.filters {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  margin-bottom: 1.25rem;
  box-shadow: var(--shadow);
}

.filters-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 1rem;
  align-items: end;
}

.filters-actions { display: flex; gap: .5rem; margin-top: 1rem; }

.field { margin-bottom: 1rem; }
.field > label { display: block; font-weight: 600; font-size: .88rem; margin-bottom: .3rem; }
.field .req { color: var(--bad); margin-left: .15rem; }

input[type="text"], input[type="search"], input[type="email"], input[type="tel"],
input[type="date"], input[type="number"], input[type="password"], select, textarea {
  width: 100%;
  padding: .5rem .6rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: #fff;
  color: var(--ink);
  font: inherit;
}

textarea { resize: vertical; min-height: 5rem; }

input:focus, select:focus, textarea:focus {
  outline: 2px solid rgba(31, 95, 208, .35);
  outline-offset: 1px;
  border-color: var(--accent);
}

/* The dark theme's form controls, and they have to come after the rules above.
 *
 * The filter bar is where the stylesheet first named a white background, and a bare element
 * selector loses to it on specificity however late in the file it appears - which is exactly
 * what happened once: the buttons went dark and every dropdown in the bar stayed white.
 *
 * The same trap the other way round: `[data-theme="dark"] input` and `input[type="date"]`
 * weigh the same, so the *later* one wins.  With this block at the top of the file the
 * literal `background: #fff` below it won, and the safety-check date field in the office's
 * vehicle table - the one input on the site that is not inside a .field - was a white box
 * with faint grey text on a dark page.  Everything inside a .field escaped it, because
 * `[data-theme="dark"] .field input` is a class heavier.  Keeping the block here means a
 * form control is dark by order of the file rather than by which wrapper it happens to be in.
 */
[data-theme="dark"] .filters input,
[data-theme="dark"] .filters select,
[data-theme="dark"] .filters textarea,
[data-theme="dark"] .field input,
[data-theme="dark"] .field select,
[data-theme="dark"] .field textarea,
[data-theme="dark"] input,
[data-theme="dark"] select,
[data-theme="dark"] textarea {
  background: #1b2028;
  border-color: var(--line-strong);
  color: var(--ink);
}

.field-invalid input, .field-invalid select, .field-invalid textarea { border-color: var(--bad); }
.field-error { color: var(--bad); font-size: .85rem; margin: .3rem 0 0; }
.field-help { color: var(--muted); font-size: .82rem; margin: .3rem 0 0; }

/* A value the reader can see but cannot change, standing in for an input. */
.field-static {
  margin: 0;
  padding: .5rem .6rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
}

.checkbox { display: flex; align-items: center; gap: .5rem; font-weight: 400; }
.checkbox input { width: auto; }

/*
 * iOS centres the value inside a date field, so on a form where every other field
 * starts at the left edge it reads as a mistake rather than a style. The input has
 * to say so and so does the pseudo-element WebKit keeps the value in, which is the
 * one that actually decides it in Safari and Chrome.
 */
input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"] {
  text-align: left;
}

input[type="date"]::-webkit-date-and-time-value,
input[type="time"]::-webkit-date-and-time-value,
input[type="datetime-local"]::-webkit-date-and-time-value,
input[type="month"]::-webkit-date-and-time-value {
  text-align: left;
}

fieldset { border: 0; padding: 0; margin: 0 0 1.25rem; }
legend { font-weight: 700; font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin-bottom: .75rem; }

.form-card .grid-2 { gap: 0 1.25rem; }
.form-actions { display: flex; gap: .5rem; align-items: center; border-top: 1px solid var(--line); padding-top: 1rem; }
.stack { display: flex; flex-direction: column; }
.inline-form { display: inline; }

/* ---------------------------------------------------------- rich editor -- */

/* The compose field: a toolbar and a contenteditable surface sit over a real
   textarea, which is what the form actually submits.  With the script absent the
   toolbar and surface stay [hidden] and the textarea is the field. */
.rich-field {
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: #fff;
  overflow: hidden;
}

.rich-field textarea {
  border: 0;
  border-radius: 0;
  min-height: 16rem;
}

.rich-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem;
  align-items: center;
  padding: .4rem .5rem;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

.rich-toolbar[hidden] { display: none; }

.rich-toolbar .btn {
  background: transparent;
  border-color: transparent;
  padding: .2rem .45rem;
  font-weight: 600;
}

.rich-toolbar .btn:hover { background: rgba(31, 95, 208, .08); }

.rich-toolbar-sep {
  width: 1px;
  align-self: stretch;
  min-height: 1.4rem;
  background: var(--line);
  margin: .1rem .25rem;
}

.rich-editor {
  min-height: 16rem;
  padding: .75rem .8rem;
  overflow-wrap: anywhere;
}

.rich-editor[hidden] { display: none; }

.rich-editor:focus {
  outline: 2px solid rgba(31, 95, 208, .35);
  outline-offset: -2px;
}

.rich-editor p { margin: 0 0 .75rem; }
.rich-editor p:last-child { margin-bottom: 0; }
.rich-editor h2 { margin: 0 0 .6rem; font-size: 1.15rem; }
.rich-editor blockquote {
  margin: 0 0 .75rem;
  padding-left: .8rem;
  border-left: 3px solid var(--line-strong);
  color: var(--ink-soft);
}
.rich-editor ul, .rich-editor ol { margin: 0 0 .75rem 1.4rem; }
.rich-editor li { margin-bottom: .2rem; }
.rich-editor a { color: var(--accent); }
.rich-editor img { max-width: 100%; height: auto; }
.rich-editor hr { border: 0; border-top: 1px solid var(--line-strong); margin: 1rem 0; }

/* The attachment picker's list: what has been chosen so far, and one way to remove it. */
.attachment-picked { margin-top: .6rem; }

.attachment-list { margin: .25rem 0 .6rem; }

.attachment-list li {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.attachment-list .attachment-name {
  flex: 1 1 auto;
  overflow-wrap: anywhere;
}

.attachment-list .btn { flex: 0 0 auto; }

[data-theme="dark"] .rich-field { background: #1b2028; border-color: var(--line-strong); }
[data-theme="dark"] .rich-toolbar { background: #222833; }
[data-theme="dark"] .rich-toolbar .btn { color: var(--ink); }
[data-theme="dark"] .rich-field textarea { background: #1b2028; }

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

.btn {
  display: inline-block;
  padding: .45rem .85rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: #fff;
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  font-size: .9rem;
  cursor: pointer;
  text-align: center;
}

.btn:hover { background: #f7f8fa; text-decoration: none; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-dark); border-color: var(--accent-dark); }

.btn-danger { background: #fff; border-color: #e0b4b0; color: var(--bad); }
.btn-danger:hover { background: var(--bad-bg); }

.btn-quiet { background: transparent; border-color: transparent; color: var(--accent); }
.btn-quiet:hover { background: rgba(31, 95, 208, .08); }

.btn-small { padding: .25rem .55rem; font-size: .82rem; }
.btn-block { width: 100%; }

/* ----------------------------------------------------------------- pills -- */

.pill, .badge {
  display: inline-block;
  padding: .1rem .5rem;
  border-radius: 999px;
  font-size: .78rem;
  font-weight: 600;
  /* A variable rather than a literal grey, so the plain pill follows the theme: this is the one
     the OFFERED column's "No" uses, and it was staying light on a dark page. */
  background: var(--line);
  color: var(--ink-soft);
}

.pill-active { background: var(--good-bg); color: var(--good); }
.pill-pending { background: var(--warn-bg); color: var(--warn); }
.pill-lapsed { background: var(--bad-bg); color: var(--bad); }
.pill-resigned { background: var(--line); color: var(--muted); }
.badge-bad { background: var(--bad-bg); color: var(--bad); }

/* --------------------------------------------------------------- notices -- */

/* A message inside a page - a form that was refused, a fee run that cannot send -
   rather than the strip that used to appear at the top of every page.  No band and
   no background: a rule down the left edge and the colour of the words, which reads
   as part of the page instead of shouting across it.  The colours come from the
   theme, so they are legible in both, which the old hard-coded hex values were not. */
.notice {
  border-left: 3px solid var(--muted);
  padding: .1rem 0 .1rem .8rem;
  margin: 0 0 1rem;
  color: var(--ink-soft);
  font-size: .92rem;
}

.notice-success { border-left-color: var(--good); color: var(--good); }
.notice-error { border-left-color: var(--bad); color: var(--bad); }
.notice-warning { border-left-color: var(--warn); color: var(--warn); }
.notice-info { border-left-color: var(--accent); color: var(--accent-dark); }

.notice > .list-tight { margin: .1rem 0; }

/* --------------------------------------------------------- dialog (message) -- */

/* Where a queued message lands: the words from partials/notifications.php, moved into a
   <dialog> and opened by assets/js/app.js.  The same block stays on the page when the
   script cannot run, so it is styled to read there too - hence the plain margins. */
.dialog {
  /* 32rem is the reading width on a desk; on a phone it must be the screen minus the
     margin the dialog is centred in, or the message sits half off the side. */
  width: min(32rem, calc(100vw - 2rem));
  max-width: none;
  padding: 1.25rem 1.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 24px 48px rgba(15, 23, 42, .28);
}

/* The dimmed page behind it.  Not a colour anybody reads text against, so it does not
   need a theme of its own - it only has to make the dialog the thing in focus. */
.dialog::backdrop { background: rgba(15, 23, 42, .45); }

.dialog-title { margin: 0 0 .75rem; font-size: 1.05rem; }

.dialog-message > .notice:last-child { margin-bottom: 0; }

.dialog-actions { margin-top: 1.1rem; display: flex; justify-content: flex-end; }

.list-tight { margin: 0; padding-left: 1.1rem; }

/* A short list of names with a secondary line each: vehicles, mostly. */
.plain-list { list-style: none; margin: 0 0 .75rem; padding: 0; }
.plain-list li { padding: .45rem 0; border-bottom: 1px solid var(--line); }
.plain-list li:last-child { border-bottom: 0; }

/* ---------------------------------------------------------------- details -- */

.details { display: grid; grid-template-columns: 10rem 1fr; gap: .35rem 1rem; margin: 0; }
.details dt { color: var(--muted); font-size: .88rem; }
.details dd { margin: 0 0 .35rem; }

.notes { white-space: pre-wrap; background: #fafbfc; border: 1px solid var(--line); border-radius: var(--radius-sm); padding: .75rem; }

/* --------------------------------------------------------------- timeline -- */

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

.timeline li {
  display: flex;
  gap: .75rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line);
}

.timeline li:last-child { border-bottom: 0; }
.timeline-time { color: var(--muted); font-size: .82rem; white-space: nowrap; min-width: 8.5rem; }
.timeline-what { font-size: .9rem; }

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

.pagination { display: flex; align-items: center; gap: 1rem; justify-content: center; margin: 1rem 0; }

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

.body-bare { background: #eef1f5; }
.bare-wrap { max-width: 25rem; margin: 0 auto; padding: 1.25rem; }

/* The drag grip app.js puts in the corner of each panel on a member's page.  Generated, so
   with JavaScript off there is none and the panels are simply in the order the server
   rendered.  Floated rather than laid out, because it is inserted as the panel's first child
   and the heading flows around it. */
.panel-grip {
  float: right;
  cursor: grab;
  color: var(--muted);
  letter-spacing: -.15em;
  line-height: 1;
  padding: 0 .2rem;
  user-select: none;
}

.panel-grip:active { cursor: grabbing; }
[data-panel].is-dragging { opacity: .45; }

/* Shown on the login form only when the site has no accounts at all. */
.first-run-command {
  margin: .5rem 0 0;
  padding: .6rem .7rem;
  background: #f3f5f7;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm);
  text-align: center;
}

/* -------------------------------------------------------------- responsive -- */

/*
 * 900px, not 760: an iPad held upright is 768px wide and an iPad Air 820, so every
 * tablet in portrait fell just outside the old breakpoint.  The tables then had no
 * scrolling and no minimum width, and .card-flush clipped what did not fit - the
 * members list lost its right-hand columns with nothing to say they were there.
 *
 * The scrolling itself now lives on .card-flush at every width, because a container
 * that scrolls only when it needs to costs nothing when it does not.  Tables are
 * left to size themselves: width: 100% fills the card when the columns fit, and a
 * table whose columns cannot shrink below the screen simply overflows and swipes.
 * A blanket min-width would push even a narrow table off the side of a phone.
 */
@media (max-width: 900px) {
  /* The club's name and who is signed in on the first line, the actions against its
     right-hand edge, and the menu on a line of its own underneath.  The order is set
     here rather than in the markup, so the source keeps brand, menu, actions - the order
     they are read and tabbed through on a desk, where they are all one row. */
  .topbar-inner { flex-wrap: wrap; }
  /* A zero basis, so the line is shared out rather than wrapped: with its natural width
     the club's name would take the whole line and push the actions onto the next one on
     the narrowest phones.  Given the space left over, it wraps its own name instead. */
  .topbar-inner > .brand-block { order: 1; flex: 1 1 0; min-width: 0; }
  .nav-actions { order: 2; }

  /* A full-width basis, not just `flex: 1 1 auto`: with the actions on the first line the
     menu would otherwise be squeezed into whatever was left of it rather than starting a
     line of its own. */
  .topbar-inner > .nav { order: 3; flex: 1 0 100%; min-width: 0; }
  .page-head-actions { margin-left: 0; }
  .details { grid-template-columns: 1fr; gap: 0; }
  .details dt { margin-top: .5rem; }
  .filters-actions .btn { flex: 1; }

  /* Two columns, and Details becomes an ordinary cell again: a two-row span in a
     two-column grid leaves the second column half empty. */
  .grid-member { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-member > .card-tall { grid-row: auto; }
}

/*
 * Phones.  The important thing here is the size of what you tap: a Remove button
 * was about 26px tall and a link in the navigation about 28, against the 44px that
 * a finger needs.  That is the difference between a site that works on a phone and
 * one that is merely readable on it.
 */
@media (max-width: 560px) {
  .btn { padding: .55rem .85rem; }
  .btn-small { padding: .5rem .65rem; font-size: .85rem; }
  .nav a { padding: .5rem .7rem; }
  .topbar-inner { padding: .5rem .9rem; gap: .6rem; }

  .wrap { padding: 1.1rem 1rem 2.5rem; }
  .card { padding: 1rem; }
  .card-login { margin-top: 4vh; padding: 1.35rem; }

  /* A phone is mostly used to tap these, and the desktop padding left them a little
     shorter than a fingertip. */
  input:not([type="checkbox"]):not([type="radio"]), select, textarea { padding: .65rem .6rem; }

  /* The activity list puts the time in a 8.5rem column, which on a phone leaves
     almost nothing for the sentence beside it. */
  .timeline li { flex-direction: column; gap: .15rem; }
  .timeline-time { min-width: 0; }

  /* One column, in the order the panels are written. */
  .grid-member { grid-template-columns: 1fr; }

  /* A list wider than the phone scrolls sideways - that is the design - but the buttons
     must not scroll with it. Edit and Remove sat past the right edge with nothing to say
     they were there, which is the difference between a table you can use and one you
     cannot. Pinning that cell to the edge keeps them reachable while the row slides
     underneath it. */
  .table td.actions {
    position: sticky;
    right: 0;
    z-index: 1;
    background: var(--surface);
    box-shadow: -12px 0 12px -12px rgba(15, 23, 42, .4);
  }

  /* Cell padding is the cheapest width to give back on a phone: four columns at 14px a
     side spend 112px of a 358px screen before a single value is drawn. Halving it is what
     lets the member's own lists keep all their columns instead of scrolling sideways. */
  .table th, .table td { padding: .5rem .5rem; }

  /* The member's vehicle list at phone width: `table-layout: fixed` with all four widths
     declared, because left to itself the browser gave the details whatever was over and
     "The blue one with the roof rack" came out one word to a line. Details/Notes keeps
     the widest share - it is the only column holding a sentence. */
  .table-vehicles { table-layout: fixed; }
  .table-vehicles .col-wide { width: 28%; }
  .table-vehicles .col-year { width: 14%; }
  .table-vehicles .col-details { width: 36%; }
  .table-vehicles .col-actions { width: 22%; }

  /* Two buttons side by side make the column wider than the column of data beside it, so
     they stack on a phone and the table keeps its width for names and numbers. */
  .table td.actions { white-space: normal; }
  .table td.actions .btn { display: block; margin: .25rem 0 0; }
  .table td.actions .btn:first-child { margin-top: 0; }
}

@media (prefers-reduced-motion: no-preference) {
  .btn { transition: background-color .12s ease, border-color .12s ease; }
}

/* Printing: a membership list on paper should not carry the navigation. */
@media print {
  .topbar, .footer, .filters, .page-head-actions, .pagination { display: none; }
  .card { box-shadow: none; border-color: #ccc; }
  body { background: #fff; }
}
