:root {
  --bg: #f4efe6;
  --ink: #2b2620;
  --muted: #6f6555;
  --accent: #7a2e26; /* Austin maroon */
  --accent-dark: #5c221c;
  --accent-soft: rgba(122, 46, 38, 0.07);
  --gold: #b8935a;
  --gold-soft: rgba(184, 147, 90, 0.14);
  --card: #fffdf8;
  --line: #ddd2bf;
  --line-soft: #eae1d2;
  --danger: #a13a2c;
  --danger-dark: #7a281c;
  /* The faint wash behind a destructive link on hover, as --accent-soft is
     behind an ordinary one. */
  --danger-soft: rgba(161, 58, 44, 0.09);
  --success: #2e6b3e;

  --radius: 12px;
  --radius-sm: 8px;
  --radius-xs: 6px;

  /* Playfair Display for headings and the crest gives the club's period
     feel; Source Sans 3 keeps the forms, tables and body copy readable.
     Both fall back to system faces if the webfonts don't load. */
  --font-display: "Playfair Display", Georgia, "Times New Roman", serif;
  --font-body: "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  /* Codes, volumes and publication numbers in the archives index, where a
     column of them has to line up to be scanned. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  /* Spacing scale — stick to these so rhythm stays consistent. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  /* Base text size. Nearly every size on the site is in rem, so the Text
     size control on the Me tab only has to move --font-scale and the whole
     page grows with it. 17px rather than the browser's usual 16 — this is a
     club site read by members who'd rather not squint. */
  --font-base: 17px;
  --font-scale: 1;

  /* One card width shared by the event cards and the magazine shelf, so a
     row of magazines lines up with a row of events. In rem, so cards grow
     with the chosen text size rather than squeezing larger type. */
  --card-w: 15.5rem;

  /* The height of a .btn small — its line box plus 7px padding and a 1px
     border, top and bottom. A section heading reserves this so it sits at
     the same height whether or not the member is allowed to see the action
     beside it. Keep the two in step if either changes. */
  --btn-small-h: calc(0.85rem * 1.2 + 16px);

  /* One timing for every hover that lifts a card — a tile, a photo, an
     event, a contact. The lift and the shadow have to finish together: at
     0.12s against 0.15s the card arrived before its shadow did, and the
     two-stage motion is what read as jerky. Eased out rather than `ease`,
     which starts too abruptly over so short a distance. */
  --hover-lift: 0.22s cubic-bezier(0.2, 0, 0.2, 1);

  --shadow-sm: 0 1px 2px rgba(43, 38, 32, 0.05), 0 2px 6px rgba(43, 38, 32, 0.06);
  --shadow: 0 2px 4px rgba(43, 38, 32, 0.05), 0 8px 24px rgba(43, 38, 32, 0.1);
  --shadow-lg: 0 4px 8px rgba(43, 38, 32, 0.06), 0 18px 48px rgba(43, 38, 32, 0.16);
  --ring: 0 0 0 3px rgba(184, 147, 90, 0.35);
}

* {
  box-sizing: border-box;
}

/* ---------- Scrollbars ---------- */
/* One scrollbar everywhere: the slim rounded one the event rows have always
   had, now on the page itself and on every pane that scrolls — the modal,
   the members table, the address suggestions, the open magazine.

   Two things follow from styling these, both wanted but worth knowing. The
   bar is 8px rather than the 15-or-so the platform draws, so every scrolling
   pane gets a few pixels wider; and on a Mac, where scrollbars normally
   float over the content and disappear, a styled one becomes a real one that
   takes its 8px like everywhere else. That is the price of them matching.

   The standard properties come first for Firefox, which has no ::-webkit
   pseudo-elements; Chrome and Safari take the pseudo-elements below. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 999px;
}

::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
  background: transparent;
}

html {
  font-size: calc(var(--font-base) * var(--font-scale));
}

/* Hold the scrollbar's width open whether or not the page scrolls. The
   content column is centred, so without this it slides sideways by half a
   scrollbar every time you move between a tab that scrolls and one that
   doesn't. Not on a phone: there the column already fills the width, so
   there is nothing to slide — and the reserved strip would be real estate
   taken for no reason. */
@media (min-width: 601px) {
  html {
    scrollbar-gutter: stable;
  }
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--ink);
  background:
    radial-gradient(1200px 500px at 50% -10%, #fbf7ef, transparent),
    var(--bg);
  /* dvh follows a phone's address bar as it collapses; vh is frozen at the
     taller of the two, which leaves a strip of blank page below the fold.
     The vh line stays for the browsers that don't know dvh. */
  min-height: 100vh;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

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

.hidden {
  display: none !important;
}

/* There for a screen reader, not on the screen — a control whose meaning is
   carried by an icon or by its column, where a visible label would only be
   noise (or, in the parts table, repeated 587 times). */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.view {
  min-height: 100vh;
  min-height: 100dvh;
}

/* ---------- Typography ---------- */
h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  line-height: 1.25;
  color: var(--accent-dark);
  font-weight: 700;
}

h2 {
  font-size: 1.35rem;
  letter-spacing: -0.01em;
}

h3 {
  font-size: 1.08rem;
}

h4 {
  font-size: 1rem;
}

a {
  color: var(--accent);
}

a:hover {
  color: var(--accent-dark);
}

/* ---------- Boot / login ---------- */
.boot-view {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
}

/* A spinner rather than the crest: this is shown while we find out whether
   there's a session to resume, and a turning icon says "wait" where a
   stationary badge only said "Vintage Austin Register" again. Font Awesome
   does the turning (fa-spin). */
.boot-spinner {
  font-size: 2.2rem;
  color: var(--accent);
}

.login-view {
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.login-card {
  width: 100%;
  max-width: 380px;
  padding: var(--sp-6) var(--sp-5) var(--sp-5);
  text-align: center;
  box-shadow: var(--shadow-lg);
  animation: modal-in 0.25s ease;
}

/* The card is centred for the crest and the title; a field and the sentence
   that explains it are not centred text, and read as ragged when they are. */
.login-card .form-row,
.login-card .notice {
  text-align: left;
}

/* On the sign-in card the buttons are the whole of each step, so they take
   the width rather than sitting in the middle of it. */
.login-card .btn.block {
  width: 100%;
}

.crest {
  display: inline-grid;
  place-items: center;
  width: 64px;
  height: 64px;
  margin: 0 auto var(--sp-3);
  border-radius: 50%;
  background: var(--accent);
  color: var(--card);
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 1px;
  border: 3px solid var(--gold);
  box-shadow: var(--shadow-sm);
}

.crest.small {
  width: 34px;
  height: 34px;
  margin: 0;
  font-size: 13px;
  border-width: 2px;
}

.login-card h1 {
  font-size: 1.45rem;
  margin: 0 0 var(--sp-1);
}

.tagline {
  margin: 0 0 var(--sp-5);
  color: var(--muted);
  font-style: italic;
}

/* ---------- Forms ---------- */
.stack {
  display: grid;
  gap: var(--sp-3);
  text-align: left;
}

/* Small uppercase field labels. Scoped so it never hits `label.btn`
   (the file-picker buttons) or labels that wrap a checkbox. */
.stack > label,
.form-row > label {
  display: block;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}

/* A label wrapping its own control is a sentence, not a field caption. */
.form-row > label.check-label,
.stack > label.check-label,
.choice-group > label.check-label,
.registration-paid-row > label.check-label {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  font-size: 0.95rem;
  text-transform: none;
  letter-spacing: 0;
  color: var(--ink);
  line-height: 1.45;
  cursor: pointer;
}

.check-label input[type="checkbox"],
.check-label input[type="radio"] {
  width: auto;
  margin: 0;
  flex: none;
  accent-color: var(--accent);
  transform: translateY(3px);
}

/* A short list of alternatives, each with a line saying what it means. */
.choice-group {
  display: grid;
  gap: var(--sp-2);
}

.choice-hint {
  display: block;
  font-size: 0.82rem;
  color: var(--muted);
  line-height: 1.4;
}

/* Label/control pairs inside a .stack sit tighter than the stack gap. */
.stack > label + input,
.stack > label + select,
.stack > label + textarea {
  margin-top: calc(var(--sp-2) - var(--sp-3));
}

input,
select,
textarea {
  width: 100%;
  padding: 11px 13px;
  font-size: 1rem;
  font-family: inherit;
  line-height: 1.4;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: #fff;
  color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

textarea {
  resize: vertical;
}

input::placeholder,
textarea::placeholder {
  color: #a99e8c;
}

input:hover,
select:hover,
textarea:hover {
  border-color: #cdbfa6;
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--gold);
  box-shadow: var(--ring);
}

#code {
  letter-spacing: 0.4em;
  text-indent: 0.4em;
  text-align: center;
  font-size: 1.35rem;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 11px 16px;
  font-size: 0.95rem;
  font-family: inherit;
  line-height: 1.2;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease,
    transform 0.08s ease, box-shadow 0.15s ease;
}

.btn:hover {
  border-color: var(--gold);
  box-shadow: var(--shadow-sm);
}

.btn:active {
  transform: translateY(1px);
}

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

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

.btn.primary:disabled {
  opacity: 0.55;
  cursor: default;
  box-shadow: none;
  transform: none;
}

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

.btn.danger-btn:hover {
  background: var(--danger-dark);
  border-color: var(--danger-dark);
}

.btn.link {
  background: none;
  border-color: transparent;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  padding: var(--sp-2);
}

.btn.link:hover {
  color: var(--accent-dark);
  border-color: transparent;
  box-shadow: none;
}

/* A destructive action offered as a link rather than a slab: Withdraw sits in
   a dialog whose primary button is Save, and a solid red block there would
   shout louder than the thing the member opened the form to do.
   `.btn.link.danger-btn` rather than relying on the two classes together —
   they carry the same weight, and `.btn.link` comes second, so Withdraw was
   rendering as a second Cancel in exactly the same colour. It also sits apart
   from the safe actions: the one button in a dialog that undoes something
   shouldn't be the one next to the mouse on its way to Cancel. */
.btn.link.danger-btn {
  background: none;
  border-color: transparent;
  color: var(--danger);
  margin-left: auto;
}

.btn.link.danger-btn:hover {
  color: var(--danger-dark);
  background: var(--danger-soft);
}

.btn.small {
  padding: 7px 12px;
  font-size: 0.85rem;
}

label.btn {
  /* File-picker buttons are <label>s — keep them looking like buttons. */
  text-transform: none;
  letter-spacing: 0;
  color: inherit;
  font-size: 0.85rem;
}

/* A link wearing a button reads as a button — no underline. `.btn.link` is
   the opposite case and keeps its own, being a link wearing a link. */
a.btn {
  text-decoration: none;
}

/* One consistent focus ring for every interactive element. */
.btn:focus-visible,
.tab:focus-visible,
.subtab:focus-visible,
.interest-btn:focus-visible,
.event-more-btn:focus-visible,
.event-link:focus-visible,
.photos-btn:focus-visible,
.photo-thumb:focus-visible,
.photo-nav:focus-visible,
.tile:focus-within,
.photo-back:focus-visible,
.text-size-btn:focus-visible,
.guides-back:focus-visible,
.edit-link:focus-visible,
.row-actions button:focus-visible,
.vehicle-open:focus-visible,
.register-car:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

.msg {
  margin: var(--sp-3) 0 0;
  min-height: 1.2em;
  font-size: 0.9rem;
  color: var(--muted);
}

.msg.error {
  color: var(--danger);
}

.msg.success {
  color: var(--success);
}

/* ---------- App shell ---------- */
/* ---------- Menu icons ---------- */
/* One icon per tab and subtab, ahead of the label. They earn their place on
   a strip of six: the shapes are what the eye lands on once a member knows
   the site, and the word is still there for the visit where they don't.

   Held in a box of their own width rather than sized by the glyph, so a
   narrow icon and a wide one put their labels in the same place — a row of
   subtabs otherwise reads as slightly ragged. */
.nav-icon {
  flex: none;
  width: 1.15em;
  text-align: center;
  font-size: 0.9em;
  opacity: 0.7;
}

/* The open tab's icon takes the accent, so the strip says where you are
   twice over — by colour and by the underline — rather than relying on a
   2px rule alone. */
.tab.active .nav-icon,
.subtab.active .nav-icon {
  opacity: 1;
  color: var(--accent);
}

/* Club name, menu and tab strip stay put; the panel scrolls under them.
   Below the modal's z-index, so a dialog still covers it. */
.app-head {
  position: sticky;
  top: 0;
  z-index: 20;
  box-shadow: var(--shadow-sm);
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  background: var(--card);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}

.brand-name {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--accent-dark);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.15;
}

.brand-sub {
  display: block;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- The menu under the member's name ---------- */
/* Sign out used to be a bare link in the corner, and Security a subtab of
   Me. Neither is a place anybody works: one ends the session and the other
   is visited once, when a phone is set up or replaced. Both now sit under
   the member's own name, which leaves the strip below holding only the work
   and the Me tab holding only what is theirs. */
.user-menu {
  position: relative;
  flex: none;
}

.user-menu-button {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  max-width: 100%;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}

.user-menu-button:hover,
.user-menu-button[aria-expanded="true"] {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.user-menu-button > .fa-circle-user {
  flex: none;
  font-size: 1.2rem;
  color: var(--accent);
}

/* Their name over whichever of their role or their branch says more about
   who they are in the club. Held to the width that is left, so a long name
   ellipses rather than pushing the chevron off the bar. */
.who {
  display: grid;
  min-width: 0;
  text-align: left;
  line-height: 1.25;
}

.who-name,
.who-role {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.who-name {
  font-size: 0.85rem;
  font-weight: 700;
}

.who-role {
  font-size: 0.72rem;
  color: var(--muted);
}

.user-menu-button .chevron {
  flex: none;
  font-size: 0.65rem;
  color: var(--muted);
  transition: transform 0.15s ease;
}

.user-menu-button[aria-expanded="true"] .chevron {
  transform: rotate(180deg);
}

.user-menu-panel {
  position: absolute;
  top: calc(100% + var(--sp-2));
  right: 0;
  /* Above the sticky tab strip below it, and below the modal's backdrop, so
     a dialog still covers it. */
  z-index: 30;
  min-width: 12rem;
  padding: var(--sp-1);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}

.user-menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  /* Thumb-sized: this is a menu on a phone as much as on a desktop. */
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  border: none;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
}

.user-menu-item:hover {
  background: var(--accent-soft);
  color: var(--accent-dark);
}

.user-menu-item .fa-solid {
  width: 1rem;
  flex: none;
  text-align: center;
  color: var(--accent);
  font-size: 0.9em;
}

/* A button that sits inside a sentence rather than beside one — .edit-link
   sets its own smaller size, which is wrong mid-paragraph. */
.link-btn {
  padding: 0;
  border: none;
  background: none;
  color: var(--accent);
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.link-btn:hover {
  color: var(--accent-dark);
}

.tabs {
  display: flex;
  gap: var(--sp-1);
  padding: 0 var(--sp-3);
  background: var(--card);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
  /* The one deliberate exception to the site's scrollbar: a bar under the
     tab strip would sit on its underline. It scrolls by swipe on the
     phones narrow enough to need it. */
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar {
  display: none;
}

.tab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  /* Never squeezed by the strip: it scrolls instead, which is what the
     nowrap below is for. */
  flex: none;
  padding: var(--sp-3) var(--sp-4);
  background: none;
  border: none;
  border-bottom: 3px solid transparent;
  font-family: inherit;
  font-size: 0.98rem;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.tab:hover {
  color: var(--accent-dark);
  background: var(--accent-soft);
}

.tab.active {
  color: var(--accent-dark);
  border-bottom-color: var(--accent);
  font-weight: bold;
}

.content {
  max-width: 900px;
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-7);
}

.panel {
  display: none;
}

.panel.active {
  display: block;
}

/* Never let the first block in a panel add margin on top of the
   content padding — but leave later headings their breathing room.
   #model-picker is in here because it's a wrapper that hides when a model
   is chosen: the heading inside it is the first thing in its subpanel even
   though it isn't the subpanel's own first child, and its top margin would
   otherwise collapse out through the wrapper and carry the whole picker
   8px below the headings on the subtabs either side of it. */
.panel > :first-child,
.subpanel > :first-child,
#model-picker > :first-child {
  margin-top: 0;
}

.panel > h2,
.panel > h3,
.subpanel > h2,
.subpanel > h3 {
  margin-bottom: var(--sp-3);
}

/* .row-label sets its own tighter rhythm — don't override it. */
.panel > h2:not(:first-child),
.panel > h3:not(.row-label):not(:first-child),
.subpanel > h2:not(:first-child),
.subpanel > h3:not(.row-label):not(:first-child) {
  margin-top: var(--sp-6);
}

/* ---------- Section headings, with or without a trailing action ---------- */
/* Every section heading is one of these, whether or not it has a button —
   so the heading falls in the same place on every panel and subpanel. The
   height is reserved rather than taken from the contents: most of these
   actions are admin- or officer-only, and without this an ordinary member
   would see the heading 10px higher than an admin does, and a subpanel with
   no action at all (the Library's technical contacts) higher again. */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: var(--btn-small-h);
  margin: var(--sp-6) 0 var(--sp-3);
}

.section-head h2,
.section-head h3 {
  margin: 0;
}

/* An action with no visible heading to sit beside. On a row of its own it
   would be nothing but a band of empty space across the top of the panel,
   so it goes to the right of the content it acts on instead, top-aligned
   with it — and the content starts level with the subtab row on every
   other tab. The content keeps whatever width is left; only the button is
   held to its own size. */
.with-action {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
}

.with-action > :first-child {
  flex: 1 1 auto;
  min-width: 0;
}

/* Pushed to the far right even where the content beside it is narrower
   than the panel — the spares contact card is capped at 320px. */
.with-action > .btn {
  flex: 0 0 auto;
  margin-left: auto;
}

/* The club's privacy statement and breach plan, under a member's own
   details — the two things they are both about. Set as links rather than
   buttons: they lead somewhere to read, they don't do anything. */
.policy-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin: var(--sp-4) 0 0;
}

.policy-links .edit-link {
  font-size: 0.85rem;
}

/* An eyebrow label rather than a display heading — keep it in the body
   face so it matches the other small uppercase captions. */
.row-label {
  margin: var(--sp-5) 0 var(--sp-2);
  font-family: var(--font-body);
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: normal;
  text-transform: uppercase;
  letter-spacing: 0.09em;
}

.placeholder {
  color: var(--muted);
  font-style: italic;
}

/* ---------- Me: details list ---------- */
.details {
  margin: 0;
  padding: var(--sp-1) var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.details > div {
  display: grid;
  grid-template-columns: 11rem 1fr;
  gap: var(--sp-3);
  align-items: baseline;
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--line-soft);
}

.details > div:last-child {
  border-bottom: none;
}

.details dt {
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.details dd {
  margin: 0;
  font-size: 1rem;
  overflow-wrap: anywhere;
}

/* ---------- Me: text size ---------- */
.text-size-options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin: var(--sp-3) 0 var(--sp-6);
}

.text-size-btn {
  display: grid;
  justify-items: center;
  gap: var(--sp-1);
  min-width: 6rem;
  padding: var(--sp-3) var(--sp-4);
  background: var(--card);
  border: 2px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: inherit;
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.text-size-btn:hover {
  border-color: var(--gold);
}

.text-size-btn.active {
  border-color: var(--accent);
  box-shadow: var(--shadow), var(--ring);
}

/* Each sample sets its own size inline, so the button shows what it does. */
.text-size-sample {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1;
  color: var(--accent-dark);
}

.text-size-name {
  font-size: 0.85rem;
  color: var(--muted);
}

.text-size-btn.active .text-size-name {
  color: var(--accent);
  font-weight: 600;
}

@media (max-width: 520px) {
  .details > div {
    grid-template-columns: 1fr;
    gap: var(--sp-1);
  }
}

/* ---------- Plain list ---------- */
.plain-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--sp-2);
}

.plain-list li {
  padding: var(--sp-3) var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* Branch name on the left, its Rename/Delete actions on the right, with the
   read-only branch posts underneath. */
.branch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

.branch-name {
  font-weight: 600;
}

.branch-row .row-actions {
  display: flex;
  gap: var(--sp-3);
  flex-shrink: 0;
}

.branch-posts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--sp-3);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}

.branch-post-title {
  display: block;
  margin-bottom: var(--sp-1);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}

.branch-post .contact-line {
  font-size: 0.88rem;
}

/* Per-row problems from a CSV import — usually a handful of lines out of
   a long file, so keep it scrollable rather than pushing the buttons off
   the bottom of the modal. */
.import-problems {
  list-style: none;
  margin: var(--sp-2) 0 0;
  padding: 0;
  max-height: 220px;
  overflow-y: auto;
  display: grid;
  gap: var(--sp-2);
  font-size: 0.88rem;
}

.import-problems li {
  padding: var(--sp-2) var(--sp-3);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
}

#branch-list-error {
  margin-top: var(--sp-2);
}

/* ---------- Contact cards ---------- */
/* Two or four posts filling the width of the Me tab — a set, not a shelf,
   so these stretch. */
.contact-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-3);
}

/* The Library's technical contacts are a shelf, and they sit one subtab
   along from the magazine and car model shelves — so any difference in the
   column rhythm shows up as the whole list shifting when you switch between
   them. Stretched columns and a tighter gap put the second card 8px and the
   third 17px right of where the tiles beside them sit. Same fixed column
   and same gap as those grids and the event grid, and they line up. */
.contact-shelf {
  grid-template-columns: repeat(auto-fill, var(--card-w));
  gap: var(--sp-4);
}

.contact-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--sp-4);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--hover-lift), border-color var(--hover-lift);
}

.contact-card:hover {
  box-shadow: var(--shadow);
  border-color: var(--gold);
}

.contact-card h3,
.contact-card h4 {
  margin: 0 0 var(--sp-2);
  font-size: 0.95rem;
}

.contact-line {
  margin: 0;
  font-size: 0.92rem;
  /* An email address has nowhere to break, and a card is only a column
     wide — let a long one break rather than run out over the edge. */
  overflow-wrap: anywhere;
}

.contact-line + .contact-line {
  margin-top: 2px;
}

.contact-empty {
  margin: 0;
  color: var(--muted);
  font-style: italic;
  font-size: 0.9rem;
}

.contact-meta {
  margin: -2px 0 var(--sp-2);
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* Above the name on a technical contact's card, so it is the first thing
   read. Set in italic and a shade quieter than the name under it: it is
   what this person can help with, not who they are. */
.contact-notes {
  margin: 0 0 var(--sp-2);
  color: var(--ink);
  font-style: italic;
}

.contact-card .contact-notes + h3 {
  margin-top: 0;
}

/* How far a file has got on its way to the server, above the dialog's own
   buttons. Only drawn while something is actually going out — an ordinary
   save has nothing to measure and the button alone says it is working. */
.save-progress {
  height: 0.4rem;
  margin: 0 0 var(--sp-3);
  border-radius: 999px;
  background: var(--line-soft);
  overflow: hidden;
}

.save-progress-bar {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.2s ease;
}

/* A list of people to tick, with a search over it. Scrolls at a few rows
   rather than growing to the height of the club: the whole membership in a
   dialog would push the Save button off the screen. */
.picker-list {
  max-height: 13rem;
  overflow-y: auto;
  padding: var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
}

.picker-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-1);
  border-radius: var(--radius-xs);
  cursor: pointer;
}

.picker-row:hover {
  background: var(--line-soft);
}

/* Which branch they are in, beside the name — two members called Bill are
   told apart by this and nothing else. */
.picker-meta {
  color: var(--muted);
  font-size: 0.82rem;
}

/* ---------- Registering for an event ---------- */
/* What one member said when they entered. A card each rather than a table
   row: most of a registration is free text — what they eat, what they need
   to get about, who is in the car — and columns would cut every one of them
   off at the same width. */
.registration-list {
  display: grid;
  gap: var(--sp-3);
}

.registration-card {
  padding: var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.registration-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
}

.registration-head h4 {
  margin: 0;
  font-size: 1rem;
}

.registration-party {
  color: var(--muted);
  font-size: 0.82rem;
  white-space: nowrap;
}

.registration-line {
  margin: 0 0 var(--sp-1);
  font-size: 0.9rem;
}

/* The field's name, quiet beside its answer — the answer is what is being
   read down the list. */
.registration-label {
  display: inline-block;
  /* Wide enough for the longest of them — "Accommodation" — so the answers
     line up down the card instead of starting wherever the label ended. */
  min-width: 9.5em;
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* What the event costs, at the top of the form somebody is about to commit
   to — not something to find out about in the email afterwards. */
.registration-cost {
  margin: 0;
  padding: var(--sp-3);
  background: var(--line-soft);
  border-radius: var(--radius-sm);
}

/* And, under it, the answer to the only question a member has after sending
   the money. Set by the organiser, so it is a statement rather than a field. */
.registration-paid {
  margin: 0;
  padding: var(--sp-3);
  background: rgba(46, 107, 62, 0.08);
  border-radius: var(--radius-sm);
  color: var(--success);
  font-size: 0.9rem;
}

/* The people travelling with them, each on their own line — indented under
   the entry so a card reads as one party rather than as several members. */
.registration-passengers {
  margin: var(--sp-2) 0;
  padding-left: var(--sp-3);
  border-left: 2px solid var(--line-soft);
}

/* A passenger's line is a label and then a name, a phone and a diet that
   together outrun a phone's width. Laid out as two columns rather than as
   one run of inline text, so the overflow wraps under the name instead of
   back to the left margin under the label. */
.registration-passengers .registration-line {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}

.registration-passengers .registration-label {
  flex: 0 0 auto;
}

.registration-sub {
  color: var(--muted);
  font-size: 0.82rem;
}

/* Everything that acts on an entry, on one line at the foot of its card and
   all to the right: paid, the day it arrived, correct it, take it off. On
   the right because the card above is read left to right and these are what
   is done about it afterwards — and together because a tick in one corner
   and two buttons in the other is one row pretending to be two. */
.registration-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line-soft);
}

/* A tick is a thing you fill in on a form; this is a thing you *do*, beside
   two buttons. So it wears a pill that turns green when it is on, and the
   checkbox itself is only there to be focused and read out — hidden rather
   than removed, so the keyboard and a screen reader still have it. */
.paid-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px var(--sp-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

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

/* Off, the tick is an outline of itself: the control shows what it would
   become rather than going blank, which is what makes it read as a switch. */
.paid-toggle i {
  opacity: 0.35;
  font-size: 0.95rem;
}

.paid-toggle:hover {
  border-color: var(--success);
  color: var(--ink);
}

.paid-toggle.is-paid,
.paid-toggle:has(input:checked) {
  background: rgba(46, 107, 62, 0.1);
  border-color: var(--success);
  color: var(--success);
  font-weight: 600;
}

.paid-toggle.is-paid i,
.paid-toggle:has(input:checked) i {
  opacity: 1;
}

/* The checkbox is off-screen, so the ring it would have drawn goes on the
   pill instead — otherwise tabbing through the list focuses nothing
   visible. */
.paid-toggle:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The date only appears once the pill is on: an empty date box beside an
   untaken tick is two ways of saying the same nothing. */
.registration-paid-date {
  /* Every input on the site is full width; this one sits in a row of
     controls, so it is only as wide as a date. */
  width: auto;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 0.85rem;
}

/* The search and the Export button over the list, on one line. */
.registration-tools {
  margin-bottom: var(--sp-3);
}

/* Not the full width of the form — it adds a row, it doesn't submit one. */
#add-passenger-btn {
  justify-self: start;
}

/* One passenger: three boxes and the button that takes them away again.
   Side by side where there is room, stacked on a phone — a name, a phone
   number and a line about what they eat are each too narrow to deserve a row
   of their own on a desktop. */
.passenger-row {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}

.passenger-fields {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr 1fr 1.4fr;
  gap: var(--sp-2);
  min-width: 0;
}

.passenger-field {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.passenger-field span {
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

@media (max-width: 620px) {
  .passenger-fields {
    grid-template-columns: 1fr;
  }

  /* Stacked, the row is tall and the remove button would sit at the bottom
     of it, a long way from the name it belongs to. */
  .passenger-row {
    align-items: flex-start;
    padding: var(--sp-3);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius-sm);
  }
}

/* ---------- Notices ---------- */
/* A short statement in a box: something has happened, or something is about
   to. The icon sits in a column of its own so a sentence that wraps doesn't
   run back underneath it. Taken from the sibling lhs-recruitment, whose
   sign-in card is the shape this one now follows. */
.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--accent-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
  line-height: 1.5;
}

.notice > .fa-solid {
  margin-top: 3px;
  flex-shrink: 0;
  font-size: 1.05em;
  color: var(--accent);
}

.notice.success {
  background: rgba(46, 107, 62, 0.08);
  border-color: rgba(46, 107, 62, 0.3);
  color: var(--success);
}

.notice.success > .fa-solid {
  color: var(--success);
}

/* ---------- Search / filter bar ---------- */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}

.filter-bar input[type="search"] {
  flex: 1 1 220px;
  min-width: 0;
  padding: 9px 13px;
  font-size: 0.92rem;
}

.filter-bar select {
  flex: 0 1 190px;
  padding: 9px 13px;
  font-size: 0.92rem;
}

.filter-bar .btn {
  flex: none;
}

.filter-count {
  margin: 0 0 var(--sp-3);
  color: var(--muted);
  font-size: 0.8rem;
}

.filter-count:empty {
  display: none;
}

/* ---------- Club officer pickers ---------- */
.officer-list {
  display: grid;
  gap: var(--sp-3);
}

/* Committee, then Appointees — the two kinds of club post, named so an
   admin assigning one knows which list they are adding to. */
.officer-group {
  margin: var(--sp-5) 0 var(--sp-2);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.officer-group:first-child {
  margin-top: 0;
}

.officer-row {
  display: grid;
  grid-template-columns: 170px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

.officer-row > label {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}

.officer-current {
  grid-column: 2;
  font-size: 0.82rem;
  color: var(--muted);
}

.officer-actions {
  grid-column: 3;
  grid-row: 1;
  display: flex;
  gap: var(--sp-2);
}

.officer-current:empty {
  display: none;
}

.officer-current.officer-error {
  color: var(--danger);
}

@media (max-width: 520px) {
  .officer-row {
    grid-template-columns: 1fr auto;
  }

  .officer-actions {
    grid-column: 2;
  }

  .officer-row > select,
  .officer-current {
    grid-column: 1 / -1;
  }

  .officer-current {
    grid-column: 1;
  }
}

/* ---------- Sub-tabs ---------- */
.subtabs {
  display: flex;
  gap: var(--sp-1);
  border-bottom: 1px solid var(--line);
  margin: 0 0 var(--sp-5);
  overflow-x: auto;
  /* Hidden for the same reason as the tab strip above it. */
  scrollbar-width: none;
}

.subtabs::-webkit-scrollbar {
  display: none;
}


.subtab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  padding: var(--sp-2) var(--sp-3);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font-family: inherit;
  font-size: 0.92rem;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.subtab:hover {
  color: var(--accent-dark);
}

.subtab.active {
  color: var(--accent-dark);
  border-bottom-color: var(--accent);
  font-weight: bold;
}

/* The strip scrolls when it has to, with the scrollbar hidden — so a label
   running off the end has nothing to say so, and the last word is simply
   cut. Admin's three ("Members", "Branches & Contacts", "Settings & logs")
   are a few pixels over on a narrow phone, so they give the padding back
   rather than the word. Below .subtab rather than above it: a media query
   carries no extra weight, and the rule it overrides would otherwise win on
   order. */
@media (max-width: 480px) {
  .subtab {
    padding-left: var(--sp-2);
    padding-right: var(--sp-2);
    gap: var(--sp-1);
  }
}

.subpanel {
  display: none;
}

.subpanel.active {
  display: block;
  animation: fade-in 0.15s ease;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ---------- A quiet stepper ---------- */
/* A chevron that stays out of the way until it's wanted: faint at rest,
   in focus under the cursor. Used by the year picker and by the branch
   strip's ends, both of which are navigation a member only looks for when
   they need it. */
.quiet-step {
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  /* Its own size rather than the icon's: a chevron that hasn't painted yet
     would otherwise leave a control with no height to press. */
  min-height: 26px;
  padding: 0;
  border: none;
  background: none;
  color: var(--muted);
  font-size: 0.8rem;
  opacity: 0.4;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease;
}

.quiet-step:hover,
.quiet-step:focus-visible {
  opacity: 1;
  color: var(--accent-dark);
}

/* Nothing left in that direction. Still there, so the pair doesn't jump
   about as you step along. */
.quiet-step:disabled {
  opacity: 0.14;
  cursor: default;
}

.quiet-step:disabled:hover {
  opacity: 0.14;
  color: var(--muted);
}

/* ---------- Events ---------- */
/* The years, on the toolbar line beside Add event rather than in a strip
   of their own: one row of chrome above the events instead of two. No rule
   under it — the branch strip above has already drawn that line, and a
   second would read as a second toolbar. */
.year-strip-wrap {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  border-bottom: none;
}

/* Numbers, and a year is a shorter word than a branch name. */
.year-strip-wrap .strip-tab {
  padding: var(--sp-1) var(--sp-3);
  font-variant-numeric: tabular-nums;
}

/* The branch strip: the same underlined strip as a subtab row, with the
   chevrons that scroll it sitting inside the same underline. */
/* Events: the year, and an officer's add action, on one line under the
   branch strip — the year belongs with the branch whose events it counts,
   and the strip is left to open the panel where every other tab's subtab
   row does. The height is reserved so a member without the action sees the
   year in the same place an officer does. */
.events-toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--btn-small-h);
  margin-bottom: var(--sp-3);
}

.events-toolbar .btn {
  margin-left: auto;
}

.strip-wrap {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--line);
  margin: 0 0 var(--sp-5);
}

.strip {
  display: flex;
  gap: var(--sp-1);
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  /* Hidden for the same reason as the tab strips above it. */
  scrollbar-width: none;
}

.strip::-webkit-scrollbar {
  display: none;
}

/* Both ends reachable already — the chevrons would only be decoration. */
.strip-wrap.no-scroll .strip-nav {
  display: none;
}

/* Louder than an ordinary stepper. These two are the only thing saying a
   name has been cut off at the edge of the strip, so at the stepper's usual
   0.4 they were easy to read as part of the rule rather than as a way on. */
.strip-nav {
  opacity: 0.85;
  color: var(--accent);
}

/* And the strip itself fades out where it carries on, so "there is more
   this way" is visible without having to notice a 26px chevron. The mask is
   only on the side there is more on: .at-start and .at-end are set from
   markStripEnds, which already knows. */
.strip-wrap:not(.no-scroll) .strip {
  mask-image: linear-gradient(to right, transparent 0, #000 1.5rem, #000 calc(100% - 1.5rem), transparent 100%);
}

.strip-wrap.at-start .strip {
  mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent 100%);
}

.strip-wrap.at-end .strip {
  mask-image: linear-gradient(to right, transparent 0, #000 1.5rem);
}

.strip-wrap.at-start.at-end .strip {
  mask-image: none;
}

/* One item in a strip — a branch, a year. Same shape as a subtab, since
   that's what it is. */
.strip-tab {
  padding: var(--sp-2) var(--sp-3);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font-family: inherit;
  font-size: 0.92rem;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}

.strip-tab:hover {
  color: var(--accent-dark);
}

.strip-tab.active {
  color: var(--accent-dark);
  border-bottom-color: var(--accent);
  font-weight: bold;
}

/* Every branch but your own and the club-wide row: there to be found, not
   to compete with the two a member came for. Under the cursor, or once
   opened, they read like any other. */
.strip-tab.dim {
  opacity: 0.45;
}

.strip-tab.dim:hover,
.strip-tab.dim:focus-visible,
.strip-tab.dim.active {
  opacity: 1;
}

/* A group's name above its own row — a car model on the register, a
   category in the archives. */
.branch-label {
  margin: var(--sp-4) 0 var(--sp-1);
  font-size: 1rem;
}

.branch-label:first-child {
  margin-top: var(--sp-1);
}

/* A row's name and its action on one line — the archives' categories. */
.row-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
}

.row-head .row-label,
.row-head .branch-label {
  margin-bottom: 0;
}

/* The model's name with its share of the register under it, rather than
   the two side by side: the name is what the eye lands on, and the count
   reads as a caption to it. */
.row-title {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: var(--sp-3);
}

.row-title .branch-label {
  margin: 0;
}

/* A caption beside a heading, not part of it — so it keeps the body face
   and a normal weight even inside a display-face heading, where it would
   otherwise come out bold serif and read as a second title. */
.row-count {
  font-family: var(--font-body);
  font-weight: 400;
  color: var(--muted);
  font-size: 0.85rem;
}

/* One group on its own: every event at once, in the same column width as
   the magazine and car model shelves. */
.event-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--card-w));
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}

.event-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--hover-lift), border-color var(--hover-lift);
}

.event-card:hover {
  box-shadow: var(--shadow);
  border-color: var(--gold);
}

/* The first event still to come. The whole year is on the page, past
   first, so this is where it is scrolled to — the gold edge and the label
   say why the page has landed part-way down. The margin keeps the card off
   the very top of the window once it has. */
.event-card.next-up {
  border-color: var(--gold);
  box-shadow: var(--shadow);
  scroll-margin-top: var(--sp-5);
}

.event-card.next-up::before {
  content: "Next up";
  color: var(--gold);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.event-card.past {
  opacity: 0.68;
}

.event-card.past:hover {
  opacity: 1;
}

.event-card h4 {
  margin: 0;
}

/* When and where — the two things people actually scan an event for, so
   they sit above the body copy in size and carry an icon to find them by. */
.event-meta {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}

.event-meta i {
  flex: 0 0 1em;
  color: var(--accent);
  font-size: 0.95em;
  text-align: center;
}

.event-about {
  font-size: 0.88rem;
  flex: 1;
}

/* A long write-up is cut to its opening lines so one wordy event doesn't
   set the height of every card beside it. Only applied when there's more
   text than fits, and always paired with the Show more button, which opens
   the whole thing in a modal. */
.event-about.clamped {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
  /* -webkit-box ignores flex-grow, so hold the four lines open. */
  flex: none;
  /* The trimmed text opens the same modal as its Show more button. */
  cursor: pointer;
}

.event-about.clamped:hover {
  color: var(--accent-dark);
}

.event-more-btn {
  align-self: flex-start;
  margin-top: calc(var(--sp-1) * -1);
  padding: 0;
  border: none;
  background: none;
  font-family: inherit;
  font-size: 0.82rem;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.event-more-btn:hover {
  color: var(--accent-dark);
}

/* Where the full details live — an entry form, a venue page. Reads exactly
   like the date and location rows above it, icon and all: it's one more
   fact about the event, so it isn't dressed as a link. Hovering colours it
   in, which is where it admits to being one. */
.event-link {
  align-self: flex-start;
  color: inherit;
  text-decoration: none;
}

.event-link:hover {
  color: var(--accent);
}

/* The whole event, opened from Show more. Same meta rows as the card, but
   with a dialog's width to read the description in. */
.event-details {
  display: grid;
  gap: var(--sp-3);
  justify-items: start;
}

/* Unlike the card, this honours the line breaks whoever wrote the event
   put in — there's room here for a paragraph or an itinerary. */
.event-details-about {
  margin: 0;
  white-space: pre-wrap;
}

.event-details .placeholder {
  margin: 0;
}

/* A quiet note under a form control explaining what it's for. */
.field-hint {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--muted);
}

/* Interest and Photos sit on one line, wrapping to two on a narrow card. */
.event-card-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-1);
}

.interest-btn {
  align-self: flex-start;
  padding: 5px 12px;
  font-size: 0.82rem;
  font-family: inherit;
  border-radius: 999px;
  border: 1px solid var(--accent);
  background: none;
  color: var(--accent);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, transform 0.08s ease;
}

.interest-btn:hover {
  background: var(--accent-soft);
}

.interest-btn.active {
  background: var(--accent);
  color: #fff;
}

.interest-btn.active:hover {
  background: var(--accent-dark);
}

.interest-btn:active {
  transform: scale(0.97);
}

/* Only shown once an event has photos, so it reads as "there's something
   to look at here" rather than as another thing to fill in. */
.photos-btn {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px 12px;
  font-size: 0.82rem;
  font-family: inherit;
  border-radius: 999px;
  border: 1px solid var(--gold);
  background: none;
  color: var(--accent-dark);
  cursor: pointer;
  transition: background 0.15s ease, transform 0.08s ease;
}

.photos-btn:hover {
  background: var(--gold-soft);
}

.photos-btn:active {
  transform: scale(0.97);
}

.photos-btn i {
  color: var(--gold);
}

/* ---------- Archives ---------- */
/* The category rail. Chips rather than another .subtabs strip: the Library
   already has one above this, and two underlined strips stacked read as a
   breadcrumb rather than as a filter. Each carries its own count, which
   follows the search. */
.archive-rail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
}

.archive-tab {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: 5px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.archive-tab:hover {
  border-color: var(--gold);
  color: var(--ink);
}

.archive-tab .n {
  font-size: 0.75rem;
  color: var(--muted);
}

.archive-tab[aria-selected="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.archive-tab[aria-selected="true"] .n {
  color: rgba(255, 255, 255, 0.7);
}

/* A category the search has emptied stays on the rail — it says the search
   reached it and found nothing, which a missing chip wouldn't. */
.archive-tab.empty {
  opacity: 0.45;
}

.archive-group {
  margin-bottom: var(--sp-6);
}

.archive-group .branch-label {
  margin-top: 0;
}

/* Sortable headings. The whole cell is the control, so there's no small
   target to find. */
.archive-table th[data-sort-column] {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}

.archive-table th[data-sort-column]:hover {
  color: var(--accent);
}

.sort-arrow {
  padding-left: var(--sp-1);
  font-size: 0.6rem;
  color: var(--accent);
}

/* Sized for a typical row, not the longest one: titles and notes wrap, so a
   long entry grows taller instead of pushing Edit and Delete off the right
   edge. Codes, dates, the scan and the buttons stay on one line. */
.archive-table td:not(.mono):not(.archive-file):not(.archive-held):not(.archive-actions) {
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 8rem;
  max-width: 22rem;
  vertical-align: top;
}

.archive-table td.archive-actions {
  white-space: nowrap;
  width: 1%;
}

/* Codes, volumes and dates line up in a monospaced face; titles and prose
   stay in the body face. Which columns are which is a property of the
   column in the database, not a list of names in here. */
.archive-table td.mono {
  font-family: var(--font-mono);
  font-size: 0.9em;
  white-space: nowrap;
}

.archive-table .archive-note {
  color: var(--muted);
  font-size: 0.9em;
}

.archive-table .archive-held {
  text-align: right;
  white-space: nowrap;
}

/* The scan, when there is one. An icon rather than the filename: most of
   these are "1932 Vol 6 No 4.pdf" and the row already says that. */
.archive-table .archive-file {
  text-align: center;
  white-space: nowrap;
}

/* A button into the reading viewer, which wore a link's clothes while it
   was one. */
.archive-table .archive-file a,
.archive-table .archive-file button {
  /* A size of its own rather than whatever the icon happens to be. The
     control is an icon and a screen-reader label, so a font that hasn't
     arrived leaves nothing to press at all — and on a phone an icon alone is
     a smaller target than a thumb. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  min-height: 2rem;
  padding: 0;
  background: none;
  border: none;
  color: var(--accent);
  font-size: 1rem;
  cursor: pointer;
}

.archive-table .archive-file a:hover,
.archive-table .archive-file button:hover {
  color: var(--gold);
}

/* The current scan on the edit form, with the tickbox that removes it. */
.archive-current-file {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-2);
  font-size: 0.9rem;
}

.field-hint {
  color: var(--muted);
  font-size: 0.8rem;
}

/* A hint that the field won't be matched to anything — worth marking, not
   worth alarming anybody about. It's a perfectly good way to record a car
   the Library has never heard of. */
.field-hint-loose {
  color: var(--accent-dark);
}

/* More than one copy is worth seeing at a glance — it's what says which
   duplicates could be lent or sold. */
.copies {
  display: inline-block;
  padding: 1px 6px;
  border: 1px solid var(--gold);
  border-radius: 999px;
  background: var(--gold-soft);
  color: var(--accent-dark);
  font-size: 0.75rem;
  font-weight: 600;
}

mark {
  background: rgba(184, 147, 90, 0.35);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
}

/* On a phone a seventeen-column table is unreadable however it scrolls, so
   each row becomes a record with its column names down the side. The label
   comes from the column itself via data-label, so a column added in Admin
   is labelled here without this stylesheet knowing about it. */
@media (max-width: 700px) {
  .archive-table thead {
    display: none;
  }

  .archive-table,
  .archive-table tbody,
  .archive-table tr,
  .archive-table td {
    display: block;
    width: 100%;
  }

  .archive-table tr {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card);
    padding: var(--sp-3);
    margin-bottom: var(--sp-3);
  }

  .archive-table td {
    border: 0;
    padding: 2px 0;
    display: flex;
    gap: var(--sp-2);
  }

  .archive-table td:empty {
    display: none;
  }

  .archive-table td::before {
    content: attr(data-label);
    flex: 0 0 7rem;
    color: var(--muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding-top: 3px;
  }

  .archive-table td.archive-held,
  .archive-table td.archive-actions {
    text-align: left;
  }
}

/* ---------- Vehicles ---------- */
/* The register's cards. Smaller than the standard card width so more fit
   in a row at once. Unlike a tile, the photo here is a landscape band
   rather than a square: a car photographed side-on loses its ends in a
   square crop, which is exactly the part anyone is looking at. */
.vehicle-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, 13rem);
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}

.vehicle-card {
  display: flex;
  flex-direction: column;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--hover-lift);
}

/* The gold edge is a ring drawn over the border, not a change to it, so
   nothing under the pointer can be nudged by hovering. */
.vehicle-card:hover {
  box-shadow: var(--shadow), 0 0 0 1px var(--gold);
}

/* The whole card above the actions is one button: a car is a thing to
   open, and asking members to find a small "View" would be worse. */
.vehicle-open {
  display: flex;
  flex-direction: column;
  gap: 0;
  flex: 1;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.vehicle-photo {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--line-soft);
}

/* A car with no photo yet still reads as a card rather than an empty box. */
.vehicle-photo-empty {
  display: grid;
  place-items: center;
  color: var(--gold);
  font-size: 2rem;
}

.vehicle-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
}

.vehicle-name {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.25;
}

.vehicle-year {
  color: var(--muted);
  font-weight: 400;
}

.vehicle-line {
  color: var(--muted);
  font-size: 0.88rem;
}

.vehicle-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-2);
  margin-top: var(--sp-1);
}

/* What the car is doing, and — on your own cards only — whether the club
   can see it. Both are pills rather than plain text so they read at a
   glance down a column of cards. */
.vehicle-status,
.vehicle-visibility {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  border: 1px solid transparent;
}

.vehicle-status {
  background: var(--gold-soft);
  border-color: var(--gold);
  color: var(--accent-dark);
}

.vehicle-status.restoration {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-dark);
}

/* Off the road is the quiet one — stated, not announced. */
.vehicle-status.off_road {
  background: none;
  border-color: var(--line);
  color: var(--muted);
}

.vehicle-visibility.shared {
  background: rgba(46, 107, 62, 0.1);
  border-color: rgba(46, 107, 62, 0.4);
  color: var(--success);
}

.vehicle-visibility.private {
  background: none;
  border-color: var(--line);
  color: var(--muted);
}

/* Edit, Photos and Delete under a card.
   These carry .row-actions, but that class only styles its buttons inside
   .data-table — a table cell can't be a flex container without breaking the
   table. Out here they were falling through to the browser's own grey
   boxes, which looked nothing like the rest of the site. So they're styled
   as the quiet text actions used inside a guide and a car model heading,
   with a hairline above to separate them from the card's own body. */
.vehicle-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 var(--sp-4);
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--line-soft);
}

.vehicle-actions button:not(.icon-btn) {
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 0.82rem;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  transition: color 0.15s ease;
}

.vehicle-actions button:not(.icon-btn):hover {
  color: var(--accent-dark);
}

/* Delete sits at the far end, away from the two that get pressed often —
   the same reason the magazine viewer keeps Close apart from its pager.
   Its colour comes from .icon-btn.delete, like every other delete. */
.vehicle-actions .icon-btn.delete {
  margin-left: auto;
}

/* ---------- The register ---------- */
/* One band per branch: the branch's name, how many members are in it, then
   one row per member. */
.register-branch {
  margin-bottom: var(--sp-6);
}

.register-members {
  display: grid;
  gap: var(--sp-3);
}

/* Who they are on the left, what they drive on the right — and on a narrow
   screen the one under the other, which is why this is a grid of named
   columns rather than a flex row: the breakpoint only has to restate the
   template. */
.register-member {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: var(--sp-3) var(--sp-4);
  align-items: start;
  padding: var(--sp-3) var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--hover-lift), border-color var(--hover-lift);
}

.register-member:hover {
  box-shadow: var(--shadow);
  border-color: var(--gold);
}

.register-person {
  min-width: 0;
}

.register-name {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--accent-dark);
}

/* The branch and the phone number, in that order: the branch says where the
   member is and the number says how to reach them. Wrapping rather than
   truncating — a phone number cut off mid-way is worse than a second line. */
.register-meta {
  margin: 2px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--sp-2);
  color: var(--muted);
  font-size: 0.88rem;
}

/* ---------- Trophies ---------- */
/* What the club has given a member, under their branch and phone. In the
   club's gold rather than the maroon everything else is in: a trophy is the
   one thing on the register that is an honour rather than a fact, and it
   should catch the eye going down a page of names. */
.register-awards {
  margin: var(--sp-2) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.award-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px var(--sp-2);
  border: 1px solid var(--gold);
  border-radius: 999px;
  background: var(--gold-soft);
  color: var(--ink);
  font-size: 0.76rem;
  /* A pill is a pill: let a two-word trophy wrap and the 999px radius turns
     it into a lozenge with the text falling out. */
  white-space: nowrap;
}

.award-tag .fa-trophy {
  color: var(--gold);
  font-size: 0.85em;
}

.award-year {
  color: var(--muted);
}

/* ---------- Trophies, on the member dialog ---------- */
.trophy-list {
  margin: var(--sp-4) 0;
  padding: 0;
  list-style: none;
}

/* A row of the list an admin edits, rather than the pill the register draws:
   the name here can be sixty characters and the column on a phone is narrow,
   and a pill whose text wraps is a lozenge with the words falling out of it.
   Plain text wraps and shrinks, and Remove drops below it when there is no
   room beside. */
.trophy-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--line-soft);
}

.trophy-held {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  font-size: 0.95rem;
}

.trophy-held .fa-trophy {
  flex: none;
  color: var(--gold);
  font-size: 0.85em;
}

.trophy-row:last-child {
  border-bottom: none;
}

/* The trophy and the year on one row with the button, rather than a form
   stacked three deep: an admin adding a year of cups is doing it several
   times over, and the fields are short. It stacks on a phone, where two
   inputs and a button across is a row of slivers. */
.trophy-add {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line-soft);
}

.trophy-add .form-row {
  display: grid;
  gap: var(--sp-1);
  margin: 0;
  min-width: 0;
}

/* The name takes what is left; the year only ever holds four digits. */
.trophy-add .form-row:first-child {
  flex: 1 1 auto;
}

.trophy-add .form-row:nth-child(2) {
  flex: 0 0 7rem;
}

.trophy-add .btn {
  flex: none;
}

@media (max-width: 600px) {
  .trophy-add {
    flex-wrap: wrap;
  }

  .trophy-add .form-row:first-child {
    flex: 1 0 100%;
  }
}

/* A number to ring, on a phone as well as on a desktop: the whole point of
   putting it in the register is that somebody uses it. Underlined only on
   hover, so a column of them doesn't read as a wall of links. */
.register-phone {
  color: var(--accent);
  text-decoration: none;
}

.register-phone:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.register-cars {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  min-width: 0;
}

/* A car beside its owner: small enough that a member with four of them is
   still one row. Pressing it opens the same details dialog a full card
   does, so the whole chip is the button. */
.register-car {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex: 1 1 13rem;
  max-width: 100%;
  padding: var(--sp-2);
  background: var(--bg);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.register-car:hover {
  border-color: var(--gold);
  background: var(--gold-soft);
}

.register-car-photo {
  display: block;
  flex: 0 0 auto;
  width: 4rem;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-xs);
  background: var(--line-soft);
}

.register-car-photo-empty {
  display: grid;
  place-items: center;
  color: var(--gold);
  font-size: 1.2rem;
}

.register-car-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-width: 0;
}

.register-car-name {
  font-family: var(--font-display);
  font-size: 0.98rem;
  font-weight: 600;
  line-height: 1.2;
}

.register-car-line {
  color: var(--muted);
  font-size: 0.84rem;
}

/* The band at the foot of the register: cars whose owner is off it. Marked
   out from the branches above it — it is a different kind of thing, and a
   reader shouldn't have to notice the wording to see that. */
.register-anonymous {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
}

.register-member.is-anonymous {
  background: transparent;
  border-style: dashed;
  box-shadow: none;
}

.register-member.is-anonymous:hover {
  box-shadow: none;
  border-color: var(--gold);
}

/* A name that isn't one: set in the body face rather than the display face
   the real names use, so the difference reads before the word does. */
.is-anonymous .register-name {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--muted);
}

/* The line under the Sharing picker, for a member who is off the register.
   Given the width of the dialog rather than the field, and a little air —
   it is a sentence, not a caption. */
.anonymous-hint {
  margin-top: var(--sp-1);
  line-height: 1.45;
}

/* A member with nothing on the register yet is still in the club — said
   plainly, in the column where their cars would be. */
.register-nocars {
  margin: 0;
  align-self: center;
  color: var(--muted);
  font-style: italic;
  font-size: 0.9rem;
}

/* The technical contacts sit above the shelf of car models, so what marks
   them off is the rule *under* them. It used to be over them, from when they
   were at the foot of the page: there, it separated them from the guides;
   here it drew a line between the subtabs and the first thing on the panel,
   and left the shelf of models butted straight up against the contacts. */
#tech-contacts-block {
  margin-bottom: var(--sp-6);
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--line);
}

/* The rule below it is the separation; the next heading's own top margin on
   top of that is a second gap doing the same job. */
#tech-contacts-block + #model-picker > .section-head {
  margin-top: 0;
}

.vehicle-details .details {
  margin-bottom: var(--sp-4);
}

.vehicle-details .photo-grid {
  margin-bottom: var(--sp-4);
}

.vehicle-details-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
  padding-right: 5.5rem;
}

/* Beside the dialog's close, in the same corner, edit first and close last. */
.vehicle-details-head .icon-btn {
  position: absolute;
  top: var(--sp-2);
  right: calc(var(--sp-2) + 1.9rem + var(--sp-2));
}

.vehicle-details-head h3 {
  margin: 0;
}

.photo-add {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 80px;
  cursor: pointer;
  color: var(--muted);
  border: 1px dashed var(--line);
  background: none;
  font-size: 1.4rem;
}

.photo-add:hover {
  color: var(--accent-dark);
  border-color: var(--accent);
}

.register-car-status {
  font-size: 0.8rem;
  color: var(--muted);
}

/* Marks off what other members never see — on the form and on the details
   alike, so the same lock means the same thing in both. */
.vehicle-private-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 var(--sp-1);
  font-size: 0.95rem;
  color: var(--muted);
}

.vehicle-private-head i {
  color: var(--gold);
}

.vehicle-notes {
  margin: 0;
  white-space: pre-wrap;
  line-height: 1.6;
}

/* A photo in the manager, with its own Remove sitting over the corner —
   the same control the magazine shelf puts over a cover. */
.photo-manage {
  position: relative;
}

.photo-manage .icon-btn {
  position: absolute;
  top: var(--sp-1);
  right: var(--sp-1);
}

/* A dialog holding a grid rather than a form — a member's whole garage in
   Admin needs the room that an ordinary 560px dialog doesn't have. Wide
   enough for three cards and the dialog's own padding, so a member with
   three cars doesn't get two and a hole; anything larger wraps, which is
   what auto-fill is for. */
.wide-modal .modal-box {
  max-width: 880px;
}

/* ---------- Admin: the member dialog ---------- */
/* One dialog per member, holding their details and their vehicles on two
   tabs. Its contents are wrapped rather than sitting directly in the modal
   box: the .subtabs strip switches the panels inside its own parent (see
   activateSubtab in app.js), and giving it a parent with no id keeps a
   dialog's tab out of the remembered-subtab store, which is for panels that
   are still there to come back to. That wrapper also puts the heading beyond
   the reach of `.modal-box > h3`, so it is said again here. */
.member-dialog > h3 {
  margin: 0 0 var(--sp-4);
  padding-right: 2.2rem;
  font-size: 1.15rem;
}

/* One width for both tabs, so the dialog doesn't resize under the admin as
   they switch. It is set by the wider of the two: three vehicle cards across,
   so a member with three doesn't get two and a hole. That is their three
   13rem tracks, the two gaps between them, the dialog's own padding, and room
   for the scrollbar the long form on the other tab brings with it.
   In rem rather than px because the cards are: at the larger text size the
   tracks grow, and this grows with them. The flat 880px of .wide-modal fits
   them at one size only, and leaves the form in the corner of a letterbox. */
.member-modal .modal-box {
  max-width: calc(39rem + 2 * var(--sp-4) + 2 * var(--sp-5) + 20px);
}

/* Held to the width of an ordinary dialog and centred in it — full-width
   inputs at 720px are a letterbox to read and a long way to drag the eye back
   from, and a Remove button that far from the trophy it removes is a reach.
   The Vehicles tab is the one that wants the whole width, for its cards. */
#member-details-panel,
#member-trophies-panel {
  max-width: 34rem;
  margin-inline: auto;
}

/* What is done to the record rather than to its fields: Reset 2FA, and
   ending or restoring the membership. Each takes a confirm of its own and
   none of them is undone by Cancel on the form, so they are ruled off below
   it rather than standing beside Save. */
.member-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line-soft);
}

.member-actions button {
  background: none;
  border: none;
  padding: 0;
  /* Thumb-sized, since this is where a phone does these too. */
  min-height: 32px;
  font-family: inherit;
  font-size: 0.85rem;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.member-actions button:hover {
  color: var(--accent-dark);
}

.member-actions .danger {
  color: var(--danger);
}

.member-actions .danger:hover {
  color: var(--danger-dark);
}

/* ---------- Library: tiles ---------- */
/* The magazine shelf and the car model picker are one component, not two
   that resemble each other: a square tile, a photo filling what's above a
   caption strip of a fixed height. Both grids build the same markup (see
   library.js) so anything changed here changes both together.

   The fixed strip is the whole point of the shape. Let it size itself and
   a two-line title shrinks its own photo, so no two tiles in a row match —
   which is exactly what a magazine's title-plus-date strip did next to a
   car model's single line of name. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--card-w));
  gap: var(--sp-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The picker sits above the guides, which need a little air. */
.model-grid {
  margin-bottom: var(--sp-5);
}

.tile {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 0;
  overflow: hidden;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  text-align: left;
  font-family: inherit;
  color: inherit;
  box-shadow: var(--shadow-sm);
  transition: border-color var(--hover-lift), box-shadow var(--hover-lift),
    transform var(--hover-lift);
}

.tile:hover {
  border-color: var(--gold);
  box-shadow: var(--shadow-lg);
  transform: translateY(-3px);
}

/* The whole photo-and-caption block is one big click target. */
.tile-open {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.tile-media {
  flex: 1;
  min-height: 0;
  width: 100%;
  display: block;
  object-fit: cover;
  background: var(--gold-soft);
}

/* Covers are portrait, so a square tile has to crop somewhere — take it
   off the foot and keep the masthead. A car photo is landscape and reads
   best from the middle, which is the default. */
.magazine-card .tile-media {
  object-position: top;
}

/* Nothing uploaded yet: the same treatment on both shelves, so a shelf
   half-filled with photos still looks like one set of things. */
.tile-placeholder {
  display: grid;
  place-items: center;
  color: var(--gold);
  font-size: 2.6rem;
}

/* Fixed height, and deliberately taller than a single line: two lines of
   title plus a line of date is the most either shelf puts here, and every
   photo above it is then exactly the same size. */
.tile-caption {
  flex: none;
  height: 4.6rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Indented like the text in an event card or a contact card, so a
     tile's title starts on the same line down the page as theirs. Only
     the sides — the strip is a fixed height and --sp-4 top and bottom
     would crowd two lines of title against a date. */
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--line-soft);
}

.tile-title {
  font-weight: bold;
  font-size: 0.9rem;
  line-height: 1.35;
  /* Two lines at most — a longer title is cut off rather than allowed to
     push the photo out of shape. The full one is on the tile's tooltip. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.tile-meta {
  color: var(--muted);
  font-size: 0.8rem;
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------- Edit, delete and close ---------- */
/* The three buttons that appear all over the site, as one control: a round
   icon button, the same size wherever it is, saying what it does with its
   shape and its colour on hover rather than with a word. See iconButton in
   app.js, which is what builds every one of them.

   A soft disc at rest and the action's own colour under the pointer, which
   is the magazine cover's button on a card's ground rather than a photo's.
   A bare icon with nothing behind it was tried first and the row read as
   empty — on a card of text, a pencil on its own is not obviously a thing
   you press. */
.icon-btn {
  width: 1.9rem;
  height: 1.9rem;
  flex: none;
  /* Inline, so a pair of them sits side by side in a table cell — which
     can't be a flex container without breaking the table — and beside the
     words of a heading. */
  display: inline-grid;
  vertical-align: middle;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: var(--line-soft);
  color: var(--muted);
  font-size: 0.82rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.icon-btn.edit:hover {
  background: var(--gold);
  color: #fff;
}

.icon-btn.delete:hover {
  background: var(--danger);
  color: #fff;
}

.icon-btn.close:hover {
  background: var(--line);
  color: var(--ink);
}

.icon-btn:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 1px;
}

/* Over a photo or a magazine cover there is no card to be quiet against,
   so the button brings its own ground with it. */
.icon-btn.on-media {
  background: rgba(43, 38, 32, 0.55);
  color: #fff;
}

.icon-btn.on-media:hover {
  color: #fff;
}

.icon-btn.on-media.delete:hover {
  background: var(--danger);
}

.icon-btn.on-media.edit:hover {
  background: var(--gold);
}

.icon-btn.on-media.close:hover {
  background: rgba(43, 38, 32, 0.85);
}

/* Drawn inside the button: a tile clips anything outside it, and the
   outline the tile itself takes can't say which of its buttons has the
   focus. */
.icon-btn.on-media:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -4px;
}

/* A card's actions as a row — an event's, a vehicle's, a guide's. Written
   with :where() so it carries no weight of its own: the rows that already
   say how they lay themselves out (.vehicle-actions, .guide-item, a
   branch) all still win. Table cells are left out of it on purpose — a
   <td> that becomes a flex container stops being a table cell. */
:where(.row-actions):not(td) {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

/* Admin's edit and delete over a cover rather than in a row of their own.
   A row would take its height out of the square and leave every magazine's
   cover shorter than the car model photo beside it. */
.tile-actions {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  display: flex;
  gap: var(--sp-1);
  z-index: 1;
}

/* The chosen model stays obvious after the click, since the guides it
   opens appear further down the page. */
.model-tile.active {
  border-color: var(--accent);
  box-shadow: var(--shadow), var(--ring);
}

.model-tile.active::after {
  content: "\f00c"; /* Font Awesome check */
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  width: 1.7em;
  height: 1.7em;
  display: grid;
  place-items: center;
  font-size: 0.8rem;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
}

/* Back out of one model's guides to the full grid of tiles. Sized for a
   comfortable hit target rather than as a small text link. */
.guides-back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
  padding: var(--sp-2) var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.guides-back:hover {
  background: var(--accent-soft);
  border-color: var(--gold);
  color: var(--accent-dark);
}

.car-model-header {
  display: flex;
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
  flex-wrap: wrap;
}

.car-model-photo {
  width: 160px;
  height: 120px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--line-soft);
}

/* A model with no photo yet still reads as a photo's worth of card rather
   than a grey rectangle — the same silhouette its tile in the picker uses. */
.car-model-photo-empty {
  display: grid;
  place-items: center;
  color: var(--gold);
  font-size: 2.2rem;
}

.car-model-basic-info {
  flex: 1;
  min-width: 220px;
}

.car-model-basic-info > :first-child {
  margin-top: 0;
}

.car-model-basic-info > :last-child {
  margin-bottom: 0;
}

.car-model-blurb {
  color: var(--ink);
}

/* The two ways out of a model's page — its manual, and the cars themselves.
   One row of chips, because they are the same kind of thing and two stray
   paragraphs read as neither. */
.model-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}

.model-manual,
.model-register {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  font-family: inherit;
  font-size: 0.88rem;
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.model-manual:hover,
.model-register:hover {
  border-color: var(--gold);
  background: var(--gold-soft);
  color: var(--accent-dark);
}

.model-manual i,
.model-register i {
  color: var(--gold);
  font-size: 0.9em;
}

/* The little "opens elsewhere" arrow is a note, not part of the label. */
.model-out {
  opacity: 0.6;
  font-size: 0.72em !important;
}

/* Where those cars are — after the count, and quieter than it. */
.model-register-where {
  color: var(--muted);
  font-size: 0.85rem;
}

/* ---------- The guides themselves ---------- */
.guide-section {
  margin-bottom: var(--sp-5);
}

/* The section's name and count on the left, its actions on the right. They
   used to run on from the name, which put "Delete section" a few pixels
   from the thing it deletes and left the heading hard to pick out at all. */
.guide-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  margin-bottom: var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line);
}

.guide-section-title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 700;
  color: var(--accent-dark);
}

.guide-section-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

.guide-list {
  display: grid;
  gap: var(--sp-2);
}

/* A section nobody has filled in yet, said where its guides would be. */
.guide-empty {
  margin: 0;
  padding-left: var(--sp-1);
}

.guide-item {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  padding: var(--sp-3) var(--sp-4);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.guide-item:hover {
  border-color: var(--gold);
  box-shadow: var(--shadow-sm);
}

/* The native <details> marker is a different shape on every platform, and
   none of them is the chevron everything else on this site opens behind. */
.guide-item summary {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  cursor: pointer;
  font-weight: 600;
  list-style: none;
}

.guide-item summary::-webkit-details-marker {
  display: none;
}

.guide-chevron {
  flex: none;
  width: 0.9em;
  color: var(--gold);
  font-size: 0.85em;
  transition: transform 0.18s ease;
}

.guide-item[open] .guide-chevron {
  transform: rotate(90deg);
}

.guide-item[open] > summary {
  color: var(--accent-dark);
}

.guide-item .guide-body {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line-soft);
}

.guide-item .guide-body > :first-child {
  margin-top: 0;
}

.guide-item .guide-body > :last-child {
  margin-bottom: 0;
}

.edit-link {
  background: none;
  border: none;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: normal;
  padding: 0;
}

.edit-link:hover {
  color: var(--accent-dark);
}

/* Edit/Delete row inside an expanded guide. Set off from the guide's own
   words by a hairline — without it, Delete sits directly under the last
   line of the instructions. */
.guide-item .row-actions {
  display: flex;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line-soft);
}

/* Config readout in the email test result — a plain two-column list, not
   a data grid, so it drops the table chrome. */
.email-config {
  margin: var(--sp-3) 0;
  font-size: 0.85rem;
}

.email-config td {
  padding: var(--sp-1) var(--sp-3) var(--sp-1) 0;
}

.email-config .muted {
  color: var(--muted);
}

/* ---------- Data table (admin) ---------- */
.table-wrap {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  overflow-x: auto;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

.data-table th,
.data-table td {
  text-align: left;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}

.data-table th {
  color: var(--muted);
  text-transform: uppercase;
  font-size: 0.7rem;
  font-weight: normal;
  letter-spacing: 0.08em;
  background: var(--accent-soft);
  border-bottom-color: var(--line);
}

.data-table tbody tr {
  transition: background 0.12s ease;
}

.data-table tbody tr:hover {
  background: var(--gold-soft);
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

/* This is a <td> — leave it a table-cell and space the buttons with
   margin, since display:flex would break the table layout. */
.data-table .row-actions button + button {
  margin-left: var(--sp-3);
}

.data-table .row-actions .icon-btn + .icon-btn {
  margin-left: var(--sp-1);
}

.data-table .row-actions button:not(.icon-btn) {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  font-family: inherit;
  font-size: 0.82rem;
  padding: 0;
}

.data-table .row-actions button:not(.icon-btn):hover {
  color: var(--accent-dark);
}


/* ---------- Modal ---------- */
/* The page behind a dialog stays where it was. `scrollbar-gutter: stable`
   on <html> keeps the gutter reserved even with the scroll switched off, so
   the centred column doesn't jump sideways as a dialog opens. */
body.modal-open {
  overflow: hidden;
}

.modal-root {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}

.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(43, 38, 32, 0.5);
  backdrop-filter: blur(2px);
  animation: fade-in 0.15s ease;
}

.modal-box {
  position: relative;
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-5);
  max-width: 560px;
  width: 100%;
  /* Same reason as body's min-height: on a phone 88vh is measured against
     the screen with the address bar hidden, so a dialog that just fits in
     theory has its buttons under the bar in practice. */
  max-height: 88vh;
  max-height: 88dvh;
  overflow-y: auto;
  animation: modal-in 0.16s ease;
}

@keyframes modal-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.985);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Clear of the close in the corner, so a long title doesn't run under it. */
.modal-box > h3 {
  margin: 0 0 var(--sp-4);
  padding-right: 2.2rem;
  font-size: 1.15rem;
}

/* The close every dialog carries, in the corner every dialog carries it in
   — put there by openModal rather than by each dialog. Placed outright
   rather than in the flow: the dialogs lay themselves out in every
   direction, some of them with flex, and a button in the flow would be a
   row of its own in each one. */
.modal-close {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  z-index: 2;
}

/* page-flip turns a page from the corners of the spread, and the top right
   one is exactly where this would sit — so the magazine's own control bar
   carries its close instead, with the zoom and the paging. */
.flipbook-modal .modal-close {
  display: none;
}

.modal-box > p {
  margin: 0;
}

.modal-box .stack {
  margin-top: 0;
}

/* The .stack gap already separates rows — the row itself only spaces
   its own label from its control. */
.modal-box .form-row {
  display: grid;
  gap: var(--sp-1);
  margin: 0;
}

.modal-actions {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  margin-top: var(--sp-2);
}

.modal-error {
  margin: 0;
  color: var(--danger);
  font-size: 0.85rem;
}

.modal-error:empty {
  display: none;
}

/* Confirm dialogs: message then actions, no form wrapper. */
.modal-box > p + .modal-actions {
  margin-top: var(--sp-5);
}

/* The notice shown over the how-to guides. Set as something to read rather
   than a line of small print — a little looser than body copy, with the
   accent rule down its side marking it as the point of the dialog. */
.modal-box > p.risk-notice {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--radius-xs);
  padding: var(--sp-3) var(--sp-4);
  line-height: 1.6;
}

/* A tick-box that belongs to the filter bar rather than to a form — the
   members table's "Include disabled". Sized to the controls beside it so
   the row still reads as one line. */
.filter-check {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: 0.9rem;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
}

.filter-check input {
  margin: 0;
  cursor: pointer;
}

/* A membership that has ended. Dimmed rather than struck through: the row
   is still a member's record, it just isn't current. */
.member-disabled {
  opacity: 0.6;
}

.member-disabled:hover {
  opacity: 1;
}

/* For an admin the whole row is the way in to that member. The hover tint is
   already on every .data-table row — this only says so with the cursor. */
.member-row {
  cursor: pointer;
}

/* The name inside it, as a real <button> so the row can be reached by
   keyboard and is announced as something to press, which a <tr> given a
   tabindex is not. Drawn as the row's own text with the accent on it: a list
   of names where each one is plainly the way in, rather than six columns with
   a link buried in the first. */
.member-name {
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  color: var(--accent-dark);
  text-align: left;
  cursor: pointer;
}

.member-row:hover .member-name,
.member-name:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Says what a row is, beside the name — small, quiet, and not a button. */
.state-tag {
  display: inline-block;
  margin-left: var(--sp-1);
  padding: 1px var(--sp-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--muted);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  vertical-align: middle;
  /* A pill is a pill: let the two words wrap and the 999px radius turns the
     tag into a lozenge with the text falling out of it. */
  white-space: nowrap;
}

/* ---------- Me: two-factor authentication ---------- */
/* The setup runs down the page as numbered steps rather than as a form —
   scanning a code with a phone is a physical thing to do, and the order
   matters more than the fields do. */
.security-steps {
  margin: 0 0 var(--sp-4);
  padding-left: var(--sp-5);
  line-height: 1.6;
}

.security-steps > li {
  margin-bottom: var(--sp-5);
}

.security-steps strong {
  display: block;
  margin-bottom: var(--sp-3);
}

/* White quiet zone and all: a QR code needs the light border around it to
   scan, so the card is the same white rather than the page's warm ground. */
.totp-qr {
  display: inline-block;
  padding: var(--sp-2);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  line-height: 0;
}

.totp-qr svg {
  display: block;
  width: 220px;
  height: 220px;
}

/* The typed-in fallback. Spaced and monospaced, because somebody is reading
   it a character at a time into a phone. */
.totp-secret code {
  display: inline-block;
  padding: var(--sp-2) var(--sp-3);
  background: var(--gold-soft);
  border-radius: var(--radius-xs);
  font-size: 1rem;
  letter-spacing: 0.12em;
  word-break: break-all;
}

.totp-confirm {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
}

.totp-confirm input {
  width: 8em;
  font-size: 1.1rem;
  letter-spacing: 0.18em;
  text-align: center;
}

/* Two states worth colouring: not yet done, and done. */
.security-warning,
.security-on {
  margin: 0 0 var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  line-height: 1.6;
}

.security-on {
  border-left-color: var(--gold);
  background: var(--gold-soft);
}

/* Shown once, and meant to be copied onto paper. */
.recovery-codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--sp-2);
  margin: 0 0 var(--sp-4);
  padding: var(--sp-4);
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
}

.recovery-codes code {
  font-size: 1rem;
  letter-spacing: 0.08em;
}

.recovery-low {
  color: var(--danger);
}

/* A tag for something only this reader can see — a member off the register,
   shown to the officers of their own branch. Quieter than a plain tag: it
   is a note about the row rather than a state of it. */
.state-tag.quiet {
  border-style: dashed;
  opacity: 0.75;
}

/* A tag for something that came good — a letter a delivery report says
   arrived, rather than one the site merely handed over. */
.state-tag.good {
  border-color: var(--success);
  color: var(--success);
}

/* A tag that says something is missing rather than something is set. */
.state-tag.warn {
  border-color: var(--danger);
  color: var(--danger);
}

/* One line under a field, explaining what else it will take. */
.form-hint {
  margin: calc(-1 * var(--sp-1)) 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

/* The member's name across the magazine they are reading — see watermark()
   in library.js for what it is and isn't for. Over the pages and under the
   controls, with pointer-events off so it never swallows a page turn, and
   faint enough that the magazine still reads. */
/* The stage scrolls when the book is zoomed in; the watermark must not go
   with it, so it is laid over a frame around the stage rather than inside
   it. */
.flipbook-frame {
  position: relative;
  width: 100%;
}

.reader-watermark {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  padding: var(--sp-7) var(--sp-6);
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}

/* Three, down a diagonal rather than tiled across the page: a repeat reads
   as texture, and the eye stops seeing the name in it — which is the
   opposite of what a watermark is for. */
.reader-watermark span {
  color: #000;
  opacity: 0.085;
  font-size: clamp(0.85rem, 1.9vw, 1.25rem);
  font-weight: 700;
  letter-spacing: 0.08em;
  white-space: nowrap;
  transform: rotate(-24deg);
}

.reader-watermark span:nth-child(1) {
  align-self: flex-start;
}

.reader-watermark span:nth-child(2) {
  align-self: center;
}

.reader-watermark span:nth-child(3) {
  align-self: flex-end;
}

/* ---------- Reading an archive scan ---------- */
/* What the club holds, drawn onto canvases rather than handed to the
   browser's PDF viewer — which arrives with Download and Print on its
   toolbar. See openScan() in archives.js for what this can and cannot do. */
.scan-frame {
  position: relative;
  background: #2b2620;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.scan-pages {
  /* A shape of its own while it loads, and while a scan that can't be drawn
     says so: without it the frame collapses to the height of one line and
     the dialog jumps as the pages arrive. */
  min-height: 40vh;
  max-height: min(78vh, calc(94vh - 6rem));
  overflow: auto;
  padding: var(--sp-4);
  display: grid;
  justify-items: center;
  gap: var(--sp-4);
}

.scan-page {
  width: 100%;
  max-width: 820px;
  height: auto;
  border-radius: var(--radius-xs);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
  /* A dragged canvas or image is a copy on its way to a desktop. */
  user-select: none;
  -webkit-user-drag: none;
}

.scan-msg {
  margin: var(--sp-7) 0;
  color: #f4efe6;
  text-align: center;
}

/* Printing the page prints the scan, toolbar or no toolbar. The pages go
   and a line says why — hidden rather than removed so the frame keeps its
   shape and the notice has somewhere to sit. */
@media print {
  .scan-pages,
  .flipbook-stage {
    visibility: hidden;
  }

  .scan-frame::after,
  .flipbook-frame::after {
    content: "This is the club's own material, for members to read on the site. It isn't for printing.";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-6);
    color: #000;
    text-align: center;
  }
}

/* ---------- Where you're signed in ---------- */
/* One card per live session. A card rather than a table row because the
   thing being read is "do I recognise this?", and a label, a date and an
   address read better stacked than in columns. */
.device-card {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* The browser this is being read on, marked rather than left out: a member
   counting their devices has to be able to account for all of them, and the
   one they are holding is the easiest to forget. */
.device-card.is-current {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.device-main {
  flex: 1;
  min-width: 0;
}

.device-label {
  margin: 0 0 2px;
  font-weight: 600;
}

.device-detail {
  margin: 0;
  color: var(--muted);
  font-size: 0.82rem;
}

.device-actions {
  margin-top: var(--sp-3);
}

/* ---------- Policy documents ---------- */
/* The privacy statement and the breach plan, read in the modal. These are
   documents rather than dialogs: body copy at a readable measure, headings
   that break it into things a member can find, and lists that look like
   lists. `.modal-box > p` zeroes paragraph margins for dialogs, so the
   rhythm is set inside this wrapper instead. */
.policy-doc {
  line-height: 1.6;
}

.policy-doc h4 {
  margin: var(--sp-5) 0 var(--sp-2);
  font-size: 1rem;
}

.policy-doc h4:first-child {
  margin-top: 0;
}

.policy-doc p {
  margin: 0 0 var(--sp-3);
}

.policy-doc ul {
  margin: 0 0 var(--sp-3);
  padding-left: var(--sp-5);
}

.policy-doc li {
  margin-bottom: var(--sp-2);
}

/* The line that says what the document is, before the first heading. */
.policy-doc .policy-lede {
  padding: var(--sp-3) var(--sp-4);
  background: var(--gold-soft);
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
}

.policy-doc .policy-footer {
  margin: var(--sp-5) 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.85rem;
}

/* ---------- The activity log ---------- */
/* When something happened, and the second line under who or what — the
   email address, the fields touched, how many records went out. */
.log-when {
  white-space: nowrap;
}

.log-detail {
  color: var(--muted);
  font-size: 0.8rem;
}

/* Two lines in one cell — the address under the name, the subject under the
   kind of letter, the reason under the outcome. Wrapped together because a
   stacked cell on a phone is a flex row: left loose, the second line becomes
   a flex item of its own and sits beside the first instead of under it. */
.log-lines {
  display: block;
}

/* Newer, where you are, Older — on one line under the table, with the two
   buttons at the ends so the page number sits between them and the thumb
   has somewhere to land on either side. */
.log-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
}

.log-page {
  margin: 0;
  color: var(--muted);
  font-size: 0.85rem;
  text-align: center;
}

/* A button at the end of the log is drawn and disabled rather than taken
   away: a pager that loses a button when you reach the last page moves the
   other one under your finger. */
.log-pager .btn[disabled] {
  opacity: 0.4;
  cursor: default;
}

/* The email log is the one table here that carries sentences — the
   provider's reason a letter bounced, which is the whole point of the row —
   and a table that will not wrap clips it at the edge. Let its cells wrap
   rather than widening the panel into a sideways scroll; the date keeps the
   nowrap every other table gives it. */
#email-log-table td {
  white-space: normal;
}

#email-log-table .log-when {
  white-space: nowrap;
}

/* ---------- Comboboxes ---------- */
/* A text box that suggests as you type: the address lookup on a member's
   details, and the car model picker on a vehicle. One look for both, under
   two names — .address-* came first and is left alone rather than renamed
   through code that is working. */
.address-autocomplete,
.combobox {
  position: relative;
}

.address-suggestions,
.combobox-list {
  position: absolute;
  z-index: 5;
  top: calc(100% + var(--sp-1));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--sp-1);
  list-style: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  /* Two lines per suggestion — room for roughly three before scrolling. */
  max-height: 280px;
  overflow-y: auto;
}

.address-suggestions li,
.combobox-list li {
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-xs);
  font-size: 0.88rem;
  line-height: 1.4;
  cursor: pointer;
}

.address-suggestions li:hover,
.combobox-list li:hover,
/* The arrow keys mark a suggestion the same way the pointer does, so
   there's one idea of "the one you're on" however you got there. */
.combobox-list li.highlighted {
  background: var(--gold-soft);
}


/* The full Nominatim name, kept as a quiet second line so near-identical
   short addresses can still be told apart. */
.address-suggestion-full {
  display: block;
  margin-top: 1px;
  font-size: 0.78rem;
  color: var(--muted);
}

.ql-editor {
  min-height: 160px;
  background: #fff;
  font-family: inherit;
}

.ql-toolbar.ql-snow,
.ql-container.ql-snow {
  border-color: var(--line);
}

.ql-toolbar.ql-snow {
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

.ql-container.ql-snow {
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

/* ---------- Flip-book viewer ---------- */
/* A magazine wants the room: this takes most of the window rather than
   sitting in the middle of it like an ordinary dialog. */
.flipbook-modal {
  padding: var(--sp-3);
}

.flipbook-modal .modal-box {
  max-width: 1400px;
  max-height: 96vh;
  padding: var(--sp-4);
}

/* The stage is the window onto the magazine; zooming past 100% widens the
   book inside it and this scrolls to reach the rest. */
.flipbook-stage {
  width: 100%;
  /* Whatever the box has left once the controls have taken theirs. */
  height: min(80vh, calc(96vh - 5.5rem));
  display: flex;
  align-items: flex-start;
  background: #2b2620;
  border-radius: var(--radius-sm);
  overflow: auto;
}

/* Auto-margin spacers, not justify-content: center. Centring a flex
   container's contents puts whatever overflows past the left edge out of
   reach of the scrollbar — zoom in and the inner margin of the left page
   could never be scrolled back to. These collapse to nothing once the book
   is wider than the stage, so it scrolls from edge to edge. */
.flipbook-stage::before,
.flipbook-stage::after {
  content: "";
  margin: auto;
}

#flipbook-container {
  /* Width is set by the zoom control; this is the 100% starting point. */
  width: 100%;
  margin: 0 auto;
  flex: none;
  /* page-flip writes a min-width and min-height here from its own settings.
     Those settings are chosen to pin the one-page/two-page choice for the
     whole session (see openFlipbook in library.js), not to size this box —
     the zoom owns its width, so neither floor is wanted. */
  min-width: 0 !important;
  min-height: 0 !important;
}

.flipbook-msg {
  color: #fff;
  padding: var(--sp-5);
  text-align: center;
}

/* How far along opening a magazine is: the bar fills as the PDF comes down
   and then page by page as each one is drawn. An eighty-page issue is a
   long wait to spend looking at the word "Loading". */
.flipbook-loading {
  width: min(22rem, 70%);
  margin: auto;
  padding: var(--sp-6) 0;
  text-align: center;
}

.flipbook-loading .flipbook-msg {
  padding: 0 0 var(--sp-3);
}

.flipbook-progress {
  height: 0.5rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.18);
  overflow: hidden;
}

.flipbook-progress-bar {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: var(--gold);
  transition: width 0.2s ease;
}

/* Nothing said how big the file is, so there's no figure to show: the bar
   sweeps to say it's working rather than filling to a percentage it would
   be making up. */
.flipbook-progress.unknown .flipbook-progress-bar {
  width: 35%;
  animation: flipbook-sweep 1.1s ease-in-out infinite;
}

@keyframes flipbook-sweep {
  from {
    transform: translateX(-110%);
  }
  to {
    transform: translateX(320%);
  }
}

/* Page turn on the left, zoom in the middle, Close pushed to the end so
   it's never next to the button you're tapping repeatedly. */
.flipbook-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-3);
}

.flipbook-control-group {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.flipbook-label {
  min-width: 6.5em;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Grab to move a zoomed-in page about. Only offered when there's actually
   somewhere to move it to. */
.flipbook-stage.pannable {
  cursor: grab;
}

.flipbook-stage.panning {
  cursor: grabbing;
  /* Nothing should be selected while a page is being dragged around. */
  user-select: none;
}

.flipbook-control-group + .flipbook-control-group .flipbook-label {
  min-width: 3.5em;
}

.flipbook-close {
  margin-left: auto;
}

.flipbook-controls .btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---------- Event photo gallery ---------- */
/* Wider than an ordinary dialog: the grid wants at least three photos
   across before it starts to look like a list.

   And a fixed height, not one taken from whatever is inside it. The box
   holds two faces — the grid and a single photo — and every photo is a
   different shape, so sizing to the contents made the dialog jump about as
   you stepped through them. It stays put instead, and a photo that doesn't
   fill it is backed with black. Each face manages its own scrolling. */
.gallery-modal .modal-box {
  max-width: 940px;
  height: min(88vh, 860px);
  max-height: none;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.gallery-modal .modal-box > .gallery-face,
.gallery-modal .modal-box > .photo-viewer {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* The grid scrolls; the heading above it and the buttons below stay put. */
.gallery-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.gallery-head {
  margin-bottom: var(--sp-4);
}

.gallery-head h3 {
  margin: 0;
  padding-right: 2.2rem;
  font-size: 1.15rem;
}

.gallery-count {
  margin: var(--sp-1) 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--sp-3);
}

.photo-thumb {
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--line-soft);
  overflow: hidden;
  cursor: pointer;
  transition: box-shadow var(--hover-lift), transform var(--hover-lift),
    border-color var(--hover-lift);
}

.photo-thumb:hover {
  border-color: var(--gold);
  box-shadow: var(--shadow);
  transform: translateY(-2px);
}

/* Square tiles keep the grid even whatever shape the photos are; the
   viewer shows each one whole. */
.photo-thumb img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}

.gallery-actions {
  margin-top: var(--sp-4);
}

/* An upload in flight — the label can't use :disabled the way a button can. */
.btn.disabled {
  opacity: 0.55;
  pointer-events: none;
}

/* Takes whatever height the caption and the bar leave it, and backs the
   photo with black. The arrows live here rather than inside the scrolling
   part, so they stay put while a zoomed photo is dragged about under them. */
.photo-viewer-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  background: #000;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

/* The window onto the photo: zooming past 100% makes the photo bigger than
   this and this scrolls to reach the rest, exactly as the magazine does. */
.photo-viewer-scroll {
  position: absolute;
  inset: 0;
  display: flex;
  overflow: auto;
}

/* An auto margin, not justify-content: centring a flex container's contents
   puts whatever overflows past the top and left edges out of reach of the
   scrollbar. This collapses to nothing once the photo is larger than the
   window, so it scrolls edge to edge. */
.photo-viewer-scroll img {
  flex: none;
  margin: auto;
  display: block;
  max-width: none;
  height: auto;
}

/* Grab to move a zoomed-in photo about. Only offered when there's somewhere
   to move it to — the same two classes the magazine's stage uses. */
.photo-viewer-scroll.pannable {
  cursor: grab;
}

.photo-viewer-scroll.panning {
  cursor: grabbing;
  user-select: none;
}

/* Bordered as well as filled: the arrows sit over the photo, which may be
   dark at the edges or bright, and have to be findable either way. */
.photo-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.5);
  background: rgba(43, 38, 32, 0.6);
  color: #fff;
  font-size: 1.1rem;
  cursor: pointer;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  transition: background 0.15s ease, border-color 0.15s ease;
}

.photo-nav:hover {
  background: rgba(43, 38, 32, 0.9);
  border-color: #fff;
}

.photo-nav.prev {
  left: var(--sp-3);
}

.photo-nav.next {
  right: var(--sp-3);
}

.photo-caption {
  margin: var(--sp-3) 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}

.photo-viewer-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-top: var(--sp-3);
}

.photo-viewer-bar .gallery-count {
  margin: 0;
}

/* Back to the grid. No underline: it reads as a step back through the
   gallery, like the magazine's controls, rather than as a link out of it. */
.photo-back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: none;
  border: none;
  border-radius: var(--radius-xs);
  font: inherit;
  font-size: 0.9rem;
  color: var(--accent);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.photo-back:hover {
  color: var(--accent-dark);
  background: var(--accent-soft);
}

.photo-zoom,
.photo-viewer-end {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.zoom-label {
  min-width: 3.5em;
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.photo-zoom .btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---------- Spares ---------- */
/* The one column that's prose. Everything else in a data table is short
   enough for a single line, so .data-table stops the lot from wrapping. */
.parts-table .parts-name {
  white-space: normal;
  min-width: 16rem;
}

.parts-table .parts-code {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.parts-table .parts-price {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.parts-table .parts-qty {
  width: 1%;
  text-align: right;
}

/* A category from the document, running across the table where that
   category starts — the same heading the printed list uses, so the two can
   be read side by side. */
.parts-table .parts-group th {
  padding: var(--sp-2) var(--sp-4);
  background: var(--gold-soft);
  color: var(--accent-dark);
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  border-bottom: 1px solid var(--line);
}

.parts-table tbody tr.in-cart {
  background: var(--accent-soft);
}

/* − 1 + . The buttons are square so the row height doesn't change when an
   Add turns into a stepper. */
.qty {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
}

.qty-step {
  width: 1.9rem;
  height: 1.9rem;
  flex: none;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--card);
  color: var(--accent);
  font-family: inherit;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.qty-step:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.qty-input {
  width: 3.2rem;
  padding: 4px 6px;
  text-align: center;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

/* The cart, at the end of the list's own controls. Carries how many items
   are waiting in it, and opens the request. */
.cart-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.cart-count {
  min-width: 1.35rem;
  padding: 0 5px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* An empty cart is still there to be opened, but it isn't the thing on the
   bar to press — so it drops back to an ordinary button until it holds
   something. */
.cart-btn.is-empty {
  background: var(--card);
  color: var(--muted);
  border-color: var(--line);
}

.cart-btn.is-empty:hover {
  background: var(--card);
  color: var(--ink);
}

.cart-btn.is-empty .cart-count {
  background: var(--line-soft);
}

/* ---------- Spares: the request dialog ---------- */
/* Said before the cart rather than after it: what this does, and doesn't
   do, is the thing a member most needs to know before pressing Send. */
/* Qualified by .modal-box, and it has to be: `.modal-box > p` zeroes the
   margin on every paragraph in a dialog, and outranks a bare class. The
   same goes for .request-total below. */
.modal-box > .request-note {
  margin: 0 0 var(--sp-6);
  padding: var(--sp-3) var(--sp-4);
  background: var(--gold-soft);
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
  line-height: 1.5;
}

/* Opened with nothing in it. Qualified by .modal-box for the same reason as
   the note above — `.modal-box > p` zeroes the margin on every paragraph. */
.modal-box > .cart-empty {
  margin: 0 0 var(--sp-2);
}

.request-items {
  max-height: 15rem;
  overflow-y: auto;
  margin: 0 0 var(--sp-2);
  padding: 0;
  list-style: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* Centred rather than sat on a baseline: each line carries a stepper, and
   a row of controls lines up on its middle. */
.request-items li {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  font-size: 0.9rem;
  border-bottom: 1px solid var(--line-soft);
}

.request-items li:last-child {
  border-bottom: none;
}

/* How many of this part, changeable here as well as in the list — the
   quantity is the thing most often got wrong, and the cart is where it's
   noticed. Narrower than the table's stepper, which has a column to itself. */
.request-qty {
  flex: none;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.request-qty .qty-step {
  width: 1.6rem;
  height: 1.6rem;
}

.request-qty .qty-input {
  width: 2.6rem;
  padding: 2px 4px;
}

.request-name {
  flex: 1 1 auto;
  min-width: 0;
}

.request-price {
  flex: none;
  font-variant-numeric: tabular-nums;
}

.modal-box > .request-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
  color: var(--muted);
  font-size: 0.85rem;
}

/* Only shown once a member has written over the suggested wording and then
   changed something that would have rewritten it — so the offer is made
   rather than their own words being taken away. */
.request-restore {
  margin: var(--sp-1) 0 0;
  color: var(--accent);
  font-size: 0.85rem;
}

/* A whole email rather than a form field: a little smaller than the inputs
   around it, and led a little wider apart, because it's read as prose. */
#request-message {
  font-size: 0.92rem;
  line-height: 1.5;
}

/* ---------- Spares: the document itself ---------- */
/* One continuous scroll rather than a pager — the list is read by
   skimming, and page buttons only get in the way of that. */
/* What can be done with the file itself, on one line under the subtabs —
   the same corner every other action on the site is in. */
.spares-head {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-bottom: var(--sp-3);
}

.pdf-pages {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
}

/* Holds a page's space before it's drawn, so the scrollbar doesn't jump
   about as pages arrive. */
.pdf-page {
  width: 100%;
  max-width: 900px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.pdf-page canvas {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------- Responsive ---------- */
/* The site is read on phones as much as on desks — a member standing next to
   somebody's car looking them up in the register — so everything below is
   about the same page at 360px rather than a second design for it.

   Three rules run through it: nothing may scroll sideways except the strips
   that are meant to (the tab rows, one wide table); a column of fixed-width
   cards becomes one card wide rather than overflowing; and anything meant to
   be pressed is big enough to press with a thumb. */

/* Grids built from a fixed card width — the magazine shelf, the car models,
   the events, the technical contacts, the register's cars. At a desk the
   fixed column is the point: two shelves side by side line up. On a phone
   there is only ever one column, and a fixed width wider than the screen
   (or than the text size the member has chosen) is a sideways scroll. */
@media (max-width: 700px) {
  .tile-grid,
  .event-grid,
  .contact-shelf,
  .vehicle-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(var(--card-w), 100%), 1fr));
  }

  /* Who they are, then what they drive, one under the other. */
  .register-member {
    grid-template-columns: minmax(0, 1fr);
  }

  .register-nocars {
    align-self: start;
  }
}

@media (max-width: 600px) {
  .topbar {
    padding: var(--sp-3) var(--sp-4);
  }

  /* Edit, delete and close are pressed with a thumb here, and a 30px circle
     is under what a thumb can be relied on to hit. The icon stays the size
     it is — it's the target that grows. */
  .icon-btn {
    width: 2.4rem;
    height: 2.4rem;
  }

  /* Except over a photo, where the button sits in a corner of somebody's
     picture and growing it would cover more of them. */
  .icon-btn.on-media {
    width: 2rem;
    height: 2rem;
  }

  /* The club's name and the member's don't both fit across a phone, and the
     club's is the one that says which site this is. The face and the chevron
     stay, which is enough to say there is a menu under them.
     Taken out of sight rather than out of the document: display:none would
     take it out of the accessibility tree too, and the icons beside it are
     aria-hidden, which would leave the button with no name to announce. */
  .who {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .content {
    padding: var(--sp-5) var(--sp-4) var(--sp-6);
  }

  /* The icon goes above the label rather than beside it. Six tabs with an
     icon each in a row would add about 160px to a strip that already has to
     scroll on a phone; stacked, a tab is only as wide as its word, so the
     strip comes out narrower than it was before the icons arrived (117px of
     scroll at 320px against 155px) and costs twenty pixels of height. */
  .tab {
    flex-direction: column;
    gap: 2px;
    padding: var(--sp-2) var(--sp-3);
    font-size: 0.85rem;
  }

  .tab .nav-icon {
    font-size: 1.05rem;
    width: auto;
  }

  /* Subtabs don't get the same treatment: two lines under the tab strip is
     a lot of furniture above the panel, and there are up to five of them.
     The icon steps aside and the word stays, which is the half that a
     member who doesn't know the site yet is reading anyway. */
  .subtab .nav-icon {
    display: none;
  }

  .section-head {
    flex-wrap: wrap;
  }

  /* The action beside a heading with no room left for it goes under. */
  .with-action {
    flex-wrap: wrap;
  }

  /* A branch name and its Rename/Delete: the actions don't shrink, so at the
     larger text sizes the pair is wider than a narrow phone. They go under
     the name instead. */
  .branch-row {
    flex-wrap: wrap;
  }

  .branch-name {
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .with-action > .btn {
    margin-left: 0;
  }

  .modal-root {
    padding: var(--sp-3);
  }

  .modal-box {
    padding: var(--sp-4);
  }

  /* Safari zooms the whole page when a field smaller than 16px takes focus,
     and never zooms back out — so the member is left reading the site at
     1.3x with the form off the right-hand edge. Every control the site has
     is under 16px at the standard text size (0.92rem of 17px is 15.6), so
     they are all held at 16 here. 1rem rather than 16px: a member who has
     chosen Larger is already above the threshold and should keep their
     choice, and 1rem is never less than 17px. */
  input,
  select,
  textarea {
    font-size: max(1rem, 16px);
  }

  /* A year picker or a strip chevron is a 26px target, which is fine for a
     pointer and small for a thumb. */
  .quiet-step {
    width: 34px;
    min-height: 34px;
  }

  /* The year picker and Add event were one row with the button pushed to the
     end; on a narrow screen the button goes under rather than squeezing the
     year between the chevrons. */
  .events-toolbar {
    flex-wrap: wrap;
  }

  /* A model's photo beside its basic info becomes the photo above it, full
     width, rather than a 160px stamp with a 220px column wedged next to it. */
  .car-model-photo {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
  }

  /* Except where there is no photo: a full-width 4:3 box of nothing is a
     third of a phone screen spent saying so. A short band is enough. */
  .car-model-photo-empty {
    aspect-ratio: auto;
    height: 5rem;
  }

  /* The register chip carries a count and then where those cars are, which
     is more than fits on one line of a phone. Stacked, with the detail
     under the number it belongs to. */
  .model-links {
    flex-direction: column;
    align-items: flex-start;
  }

  .model-register {
    flex-wrap: wrap;
    border-radius: var(--radius-sm);
  }

  /* The parts list stays a table — it is a price list, and the category
     bands running across it are how it matches the printed one — but the
     prose column stops reserving sixteen characters of width it doesn't
     need, so what scrolls sideways is a few tens of pixels rather than a
     third of the table. */
  .parts-table .parts-name {
    min-width: 9rem;
  }

  .data-table th,
  .data-table td {
    padding: var(--sp-3) var(--sp-3);
  }

  /* Both viewers' control bars: page/zoom/close spread across a phone is
     three cramped groups, so they centre and wrap instead. */
  .photo-viewer-bar {
    justify-content: center;
  }

  .flipbook-label {
    min-width: 0;
  }
}

/* The admin tables — members, and the activity log — read as records rather
   than as a seven-column scroll. Same shape as the archives table above, and
   the labels come from each cell's own data-label so a column added in
   admin.js is labelled here without this stylesheet knowing about it. */
@media (max-width: 760px) {
  .data-table.stacked thead {
    display: none;
  }

  .data-table.stacked,
  .data-table.stacked tbody,
  .data-table.stacked tr,
  .data-table.stacked td {
    display: block;
    width: 100%;
  }

  /* The wrapper's own card would draw a second border round the set. */
  .table-wrap.stacked-wrap {
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .data-table.stacked tr {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card);
    padding: var(--sp-3);
    margin-bottom: var(--sp-3);
  }

  .data-table.stacked tbody tr:hover {
    background: var(--card);
  }

  .data-table.stacked td {
    border: 0;
    padding: 2px 0;
    display: flex;
    gap: var(--sp-2);
    /* .data-table holds every cell to one line, which is what makes the
       seven columns scroll in the first place. */
    white-space: normal;
  }

  .data-table.stacked td:empty {
    display: none;
  }

  /* A stacked cell is a flex row, so a tag in one is a flex item and is
     stretched to the cell's height by default — which turns the 999px radius
     of a one-line pill into an ellipse as soon as the text beside it wraps to
     two lines ("Branch manager · tech", with No 2FA next to it). Hold it to
     its own height at the top of the cell. */
  .data-table.stacked td .state-tag {
    align-self: flex-start;
  }

  .data-table.stacked td::before {
    content: attr(data-label);
    flex: 0 0 6.5rem;
    color: var(--muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    padding-top: 3px;
  }

  /* The row's actions are the one cell with no column name worth printing —
     and the one that has to be thumb-sized. Wrapped as a row of text
     buttons with room round each. */
  .data-table.stacked td.row-actions {
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    margin-top: var(--sp-2);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--line-soft);
  }

  .data-table.stacked td.row-actions::before {
    display: none;
  }

  .data-table.stacked td.row-actions button:not(.icon-btn) {
    min-height: 32px;
    font-size: 0.9rem;
  }

  /* Margin between buttons is a table-cell trick; out here they are laid
     out by the gap above. */
  .data-table.stacked .row-actions button + button {
    margin-left: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Panes inside Settings & logs: the same strip as .subtab, on classes of
   their own so the global subtab handler leaves them alone. */
.pane-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  padding: var(--sp-2) var(--sp-3);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font-family: inherit;
  font-size: 0.92rem;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.pane-tab:hover {
  color: var(--accent-dark);
}

.pane-tab.active {
  color: var(--accent-dark);
  border-bottom-color: var(--accent);
  font-weight: bold;
}

.pane {
  display: none;
}

.pane.active {
  display: block;
  animation: fade-in 0.15s ease;
}

.logo {
  display: block;
  margin: 0 auto var(--sp-3);
  height: auto;
  object-fit: contain;
}

.logo.small {
  width: 44px;
  margin: 0;
}

/* The wordmark uses Fraunces, the face the club website sets its name in. */
.login-card h1,
.brand-name {
  font-family: "Fraunces", Georgia, "Times New Roman", serif;
  font-weight: 600;
}
.event-card[data-card-event] { cursor: pointer; }
#event-details-bar { margin-bottom: 0.75rem; }

.count-toggle {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-dark);
  cursor: pointer;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}

.count-toggle i {
  font-size: 0.7em;
}

.count-more {
  display: block;
  margin-top: 2px;
}

/* The magazine search is no wider than one tile, with room above and below. */
#lib-magazines .filter-bar {
  margin: var(--sp-4) 0;
}

#lib-magazines .filter-bar input[type="search"] {
  flex: 0 1 var(--card-w);
  max-width: 100%;
}
