/* DJ CRM - plain CSS, no build step. Edit freely. */

/* ============================================================
   COLOURS
   Every colour in this file comes from a variable below, so a
   theme is a swap of these values and nothing else.

   Dark is the default look. The rules underneath it switch to
   light when either (a) the Mac is set to light and no explicit
   choice has been made, or (b) Light is chosen on the
   preferences page, which writes data-theme="light" onto <html>.
   ============================================================ */

:root {
  /* --- the dark palette --- */
  --dark-bg: #14161a;
  --dark-panel: #1b1e24;
  --dark-input: #23272e;
  --dark-ink: #e8eaed;
  --dark-ink-contrast: #14161a;   /* text drawn ON --ink */
  --dark-muted: #9aa1ab;
  --dark-line: #2c3138;
  /* The field you are typing in: a clearly lifted border and a slightly
     lifted background, instead of a ring. Both are measured against the
     colours around them in test/contrast.test.js. */
  --dark-focus-line: #868f9e;
  --dark-focus-field: #2b3038;
  --dark-accent: #8b8cf7;
  --dark-accent-ink: #14161a;     /* text drawn ON --accent */
  --dark-danger: #f87171;
  --dark-danger-line: #6d2b2b;
  --dark-code-bg: #23272e;
  --dark-error-bg: #3a1d1d;
  --dark-error-line: #7f3232;
  --dark-error-ink: #fecaca;
  --dark-notice-bg: #123021;
  --dark-notice-line: #2f6b47;
  --dark-notice-ink: #a7f3c8;
  /* The floating confirmation disc, and the row that just changed.
     The two discs are the SAME hex in both themes on purpose: it is a badge
     with its own boundary rather than page furniture, and a confirmation that
     is a different green depending on the hour reads as two different marks.
     Both are measured against white, and against both backgrounds, in
     test/contrast.test.js. */
  /* THE ONE THING A CLIENT CAME TO SOUNDCHECK TO DO. Blue rather than the
     app's accent, which is Joel's indigo and belongs to his side - and blue
     because that is what Joel asked for. Different in the two themes because
     a blue that carries white text on a white page is too dark to be seen on
     a dark one; both pairs are measured in test/contrast.test.js. */
  --dark-plan-blue: #3b82f6;
  --dark-plan-blue-ink: #0b1220;
  --dark-ok-disc: #15803d;
  --dark-bad-disc: #d12b2b;
  --dark-disc-ink: #ffffff;
  --dark-settle: #262d3d;
  --dark-shadow: rgba(0, 0, 0, .55);
  --dark-backdrop: rgba(0, 0, 0, .6);
  --dark-inquiry-bg: #3a2f10;   --dark-inquiry-ink: #fbd786;
  --dark-booked-bg: #12301d;    --dark-booked-ink: #86efac;
  --dark-completed-bg: #23244d; --dark-completed-ink: #b3b8ff;
  --dark-cancelled-bg: #262a30; --dark-cancelled-ink: #aab2bd;

  /* --- the light palette --- */
  --light-bg: #f6f7f9;
  --light-panel: #ffffff;
  --light-input: #ffffff;
  --light-ink: #1a1d21;
  --light-ink-contrast: #ffffff;
  --light-muted: #5f6773;
  --light-line: #e3e6ea;
  --light-focus-line: #697180;
  --light-focus-field: #f1f4f9;
  --light-accent: #4f46e5;
  --light-accent-ink: #ffffff;
  --light-danger: #b91c1c;
  --light-danger-line: #f0d0d0;
  --light-code-bg: #eceff3;
  --light-error-bg: #fee2e2;
  --light-error-line: #fca5a5;
  --light-error-ink: #7f1d1d;
  --light-notice-bg: #dcfce7;
  --light-notice-line: #86efac;
  --light-notice-ink: #14532d;
  --light-plan-blue: #1d4ed8;
  --light-plan-blue-ink: #ffffff;
  --light-ok-disc: #15803d;
  --light-bad-disc: #d12b2b;
  --light-disc-ink: #ffffff;
  --light-settle: #e4eaf8;
  --light-shadow: rgba(15, 23, 42, .18);
  --light-backdrop: rgba(15, 23, 42, .35);
  --light-inquiry-bg: #fef3c7;   --light-inquiry-ink: #92400e;
  --light-booked-bg: #dcfce7;    --light-booked-ink: #14532d;
  --light-completed-bg: #e0e7ff; --light-completed-ink: #3730a3;
  --light-cancelled-bg: #eef0f3; --light-cancelled-ink: #4b5563;

  /* --- what the rest of the file actually uses: dark by default --- */
  color-scheme: dark;
  --bg: var(--dark-bg);
  --panel: var(--dark-panel);
  --input: var(--dark-input);
  --ink: var(--dark-ink);
  --ink-contrast: var(--dark-ink-contrast);
  --muted: var(--dark-muted);
  --line: var(--dark-line);
  --focus-line: var(--dark-focus-line);
  --focus-field: var(--dark-focus-field);
  --accent: var(--dark-accent);
  --accent-ink: var(--dark-accent-ink);
  --danger: var(--dark-danger);
  --danger-line: var(--dark-danger-line);
  --code-bg: var(--dark-code-bg);
  --error-bg: var(--dark-error-bg);
  --error-line: var(--dark-error-line);
  --error-ink: var(--dark-error-ink);
  --notice-bg: var(--dark-notice-bg);
  --notice-line: var(--dark-notice-line);
  --notice-ink: var(--dark-notice-ink);
  --plan-blue: var(--dark-plan-blue);
  --plan-blue-ink: var(--dark-plan-blue-ink);
  --ok-disc: var(--dark-ok-disc);
  --bad-disc: var(--dark-bad-disc);
  --disc-ink: var(--dark-disc-ink);
  --settle: var(--dark-settle);
  --shadow: var(--dark-shadow);
  --backdrop: var(--dark-backdrop);
  --inquiry-bg: var(--dark-inquiry-bg);     --inquiry-ink: var(--dark-inquiry-ink);
  --booked-bg: var(--dark-booked-bg);       --booked-ink: var(--dark-booked-ink);
  --completed-bg: var(--dark-completed-bg); --completed-ink: var(--dark-completed-ink);
  --cancelled-bg: var(--dark-cancelled-bg); --cancelled-ink: var(--dark-cancelled-ink);

  --radius: 10px;
  /* HOW WIDE A DATE OR A TIME BOX IS.
     Every other field in this app is as wide as the form, because what goes
     in it is a name, an address or a paragraph and there is no knowing how
     long. A date is "Jun 12, 2026" and a time is "9:30 PM" - a dozen
     characters at the outside - and a box stretched to 640px for twelve
     characters reads as a box you have got wrong rather than one you have
     filled in. These are those twelve characters plus room to breathe, and
     they are tokens rather than numbers written in eight places because the
     same two controls turn up on the gig form, the planning form, the deposit
     line and wherever the next one goes. */
  --field-date: 13rem;
  --field-time: 10.5rem;
  /* Every control in the top bar is this tall - "+ Gig", "+ Client", the
     sliders button and the search box - so the row reads as one set rather
     than four things that happen to sit side by side. */
  --control-h: 38px;
}

/* No choice saved yet + the Mac is in light mode. "data-theme" is only ever
   on the page when Dark or Light was picked deliberately, and an explicit
   Dark must beat the system here. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: var(--light-bg);
    --panel: var(--light-panel);
    --input: var(--light-input);
    --ink: var(--light-ink);
    --ink-contrast: var(--light-ink-contrast);
    --muted: var(--light-muted);
    --line: var(--light-line);
    --focus-line: var(--light-focus-line);
    --focus-field: var(--light-focus-field);
    --accent: var(--light-accent);
    --accent-ink: var(--light-accent-ink);
    --danger: var(--light-danger);
    --danger-line: var(--light-danger-line);
    --code-bg: var(--light-code-bg);
    --error-bg: var(--light-error-bg);
    --error-line: var(--light-error-line);
    --error-ink: var(--light-error-ink);
    --notice-bg: var(--light-notice-bg);
    --notice-line: var(--light-notice-line);
    --notice-ink: var(--light-notice-ink);
    --plan-blue: var(--light-plan-blue);
    --plan-blue-ink: var(--light-plan-blue-ink);
    --plan-blue: var(--light-plan-blue);
  --plan-blue-ink: var(--light-plan-blue-ink);
  --ok-disc: var(--light-ok-disc);
    --bad-disc: var(--light-bad-disc);
    --disc-ink: var(--light-disc-ink);
    --settle: var(--light-settle);
    --shadow: var(--light-shadow);
    --backdrop: var(--light-backdrop);
    --inquiry-bg: var(--light-inquiry-bg);     --inquiry-ink: var(--light-inquiry-ink);
    --booked-bg: var(--light-booked-bg);       --booked-ink: var(--light-booked-ink);
    --completed-bg: var(--light-completed-bg); --completed-ink: var(--light-completed-ink);
    --cancelled-bg: var(--light-cancelled-bg); --cancelled-ink: var(--light-cancelled-ink);
  }
}

/* Light picked on the preferences page: same swap, whatever the Mac says. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: var(--light-bg);
  --panel: var(--light-panel);
  --input: var(--light-input);
  --ink: var(--light-ink);
  --ink-contrast: var(--light-ink-contrast);
  --muted: var(--light-muted);
  --line: var(--light-line);
  --focus-line: var(--light-focus-line);
  --focus-field: var(--light-focus-field);
  --accent: var(--light-accent);
  --accent-ink: var(--light-accent-ink);
  --danger: var(--light-danger);
  --danger-line: var(--light-danger-line);
  --code-bg: var(--light-code-bg);
  --error-bg: var(--light-error-bg);
  --error-line: var(--light-error-line);
  --error-ink: var(--light-error-ink);
  --notice-bg: var(--light-notice-bg);
  --notice-line: var(--light-notice-line);
  --notice-ink: var(--light-notice-ink);
  --plan-blue: var(--light-plan-blue);
  --plan-blue-ink: var(--light-plan-blue-ink);
  --ok-disc: var(--light-ok-disc);
  --bad-disc: var(--light-bad-disc);
  --disc-ink: var(--light-disc-ink);
  --settle: var(--light-settle);
  --shadow: var(--light-shadow);
  --backdrop: var(--light-backdrop);
  --inquiry-bg: var(--light-inquiry-bg);     --inquiry-ink: var(--light-inquiry-ink);
  --booked-bg: var(--light-booked-bg);       --booked-ink: var(--light-booked-ink);
  --completed-bg: var(--light-completed-bg); --completed-ink: var(--light-completed-ink);
  --cancelled-bg: var(--light-cancelled-bg); --cancelled-ink: var(--light-cancelled-ink);
}

* { box-sizing: border-box; }

/* ============================================================
   `hidden` HAS TO ACTUALLY HIDE.

   The browser's own rule is [hidden] { display: none }, which is
   a plain attribute selector - so ANY class selector in this
   file that sets display beats it. `.form label { display:
   flex }` did exactly that, and the two fields the picker hides
   behind its combobox stayed on screen and, worse, stayed in
   the Tab order: three stops for one control, two of them
   invisible to somebody using a mouse and unavoidable for
   somebody using a keyboard.

   This is the one rule in the file that uses !important, and it
   is the case the keyword exists for: an author rule that must
   outrank every other author rule, because it is not styling -
   it is the difference between an element being there and not.
   ============================================================ */
[hidden] { display: none !important; }

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

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

/* ---------- Showing where the keyboard is ----------
   Two different jobs, done two different ways.

   BUTTONS AND LINKS get a ring, and only for the keyboard. A mouse click
   already said where you are, so a ring after a click is just noise. The
   first rule takes away the one browsers draw of their own accord; the
   second puts it back on :focus-visible, which is meant to mean exactly
   "indicate this one".

   The third rule is the belt to that pair of braces. Browsers do not all
   agree on when :focus-visible applies after a click - Safari and Chrome
   differ on <summary> in particular - so public/app.js writes
   data-input="mouse" or "key" onto <html> and this switches the ring off
   outright while the mouse is what is being used. With JavaScript off the
   attribute never appears and the browser's own judgement stands.

   TEXT BOXES do not get a ring at all - see "A field you are typing in"
   below. :focus-visible is no help there: browsers deliberately keep
   reporting it on a text box after a mouse click, which is how the loud
   ring survived the last attempt at this. */
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
html[data-input="mouse"] :focus-visible { outline: none; }

/* ---------- A field you are typing in ----------
   Somebody filling in a form has to be able to see which box their typing
   is going into, whether they got there by mouse or by Tab. That is said
   here with the field's own border and background rather than with a ring
   round the outside: the border lifts to --focus-line and the inside lifts
   to --focus-field. Quiet, but not subtle enough to miss - both colours are
   measured against their neighbours in test/contrast.test.js, which holds
   them to the 3:1 WCAG asks for a non-text indicator.

   The :not()s do two jobs. They keep out the controls this makes no sense
   for - a radio has no border of ours to shift, so it keeps the keyboard
   ring above - and they carry the weight this rule needs to beat the
   per-form rules further down the file. A bare "select:focus" weighs exactly
   the same as ".form select" and loses to it for being higher up, which is
   how a focused dropdown quietly kept its resting border the first time. */
input:not([type="radio"]):not([type="checkbox"]):focus,
select:not([disabled]):not([size]):focus,
textarea:not([disabled]):not([readonly]):focus {
  outline: none;
  border-color: var(--focus-line);
  background: var(--focus-field);
}

/* ---------- "Restart me" banner ----------
   Written by views/partials/header.ejs when src/freshness.js has noticed the
   running process is behind the files on disk. Uses the error colours on
   purpose: this is not a tip, it is the reason the page below it is wrong. */
.stale-banner {
  margin: 0; padding: 10px 24px;
  background: var(--error-bg); border-bottom: 1px solid var(--error-line);
  color: var(--error-ink); font-size: 13px; line-height: 1.5;
}
.stale-banner kbd {
  font: inherit; font-weight: 700;
  background: var(--code-bg); color: var(--ink);
  padding: 0 5px; border-radius: 4px;
}
.stale-banner code {
  background: var(--code-bg); color: var(--ink);
  padding: 1px 5px; border-radius: 4px; font-size: 12px;
}

/* ---------- Header ---------- */
.site-header {
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
  padding: 14px 24px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}
.brand { font-weight: 700; font-size: 17px; color: var(--ink); }
/* FIVE LINKS IN A ROW THAT WILL NOT BREAK IS 350 PIXELS, and a phone can be
   360 wide once its scrollbar has had its share. The bar itself already wraps,
   so the nav drops onto a line of its own - but the nav's own five links were
   one unbreakable row, and eleven pixels of it hung off the side of every
   single page in the app. The same is true of the buttons on the right.
   row-gap so the two lines are not touching when it does wrap. */
.site-header nav { display: flex; gap: 18px; flex-wrap: wrap; row-gap: 6px; }
.site-header nav a { color: var(--muted); }
.site-header nav a.active { color: var(--ink); font-weight: 600; }
.header-actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }

main { max-width: 1060px; margin: 0 auto; padding: 24px; }

h1 { font-size: 24px; margin: 0 0 4px; }
h2 { font-size: 15px; margin: 0 0 12px; text-transform: uppercase;
     letter-spacing: .04em; color: var(--muted); }

.page-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.page-actions { display: flex; gap: 8px; align-items: center; }

/* ---------- Cards ---------- */
.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  margin-bottom: 20px;
}
.card-head { display: flex; justify-content: space-between; align-items: baseline; }
.card-head h2 { margin-bottom: 12px; }

/* ---------- Stat tiles ---------- */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}
/* Counts and money are two grids so each row fills the width. Close the gap
   between them so they still read as one block of numbers. */
.stats + .stats { margin-top: -8px; }
.stat {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
  display: flex; flex-direction: column; gap: 2px; color: var(--ink);
}
a.stat:hover { border-color: var(--accent); text-decoration: none; }
.stat-value { font-size: 22px; font-weight: 700; }
.stat-label { font-size: 12px; color: var(--muted);
              text-transform: uppercase; letter-spacing: .04em; }

/* ---------- Tables ---------- */
table { width: 100%; border-collapse: collapse; }
th {
  text-align: left; font-size: 11px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted);
  padding: 8px 10px; border-bottom: 1px solid var(--line);
}
td { padding: 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
tr:last-child td { border-bottom: none; }
.col-date { white-space: nowrap; }
.col-money { text-align: right; white-space: nowrap; }
th.col-money { text-align: right; }

.muted { color: var(--muted); }
.muted-sm { display: block; font-size: 12px; color: var(--muted); }
.empty { color: var(--muted); margin: 6px 0; }

/* ---------- Status badges ---------- */
.badge {
  display: inline-block; padding: 3px 9px; border-radius: 999px;
  font-size: 12px; font-weight: 600; border: 1px solid transparent;
}
.badge-inquiry   { background: var(--inquiry-bg);   color: var(--inquiry-ink); }
.badge-booked    { background: var(--booked-bg);    color: var(--booked-ink); }
.badge-completed { background: var(--completed-bg); color: var(--completed-ink); }
.badge-cancelled { background: var(--cancelled-bg); color: var(--cancelled-ink); }

.status-switcher { display: flex; gap: 8px; flex-wrap: wrap; }
.status-switcher form { display: inline; }
.status-switcher button { cursor: pointer; font-family: inherit; }
/* Faded, but not so faded that the label stops being readable - the
   opacity here is checked by test/contrast.test.js. */
.status-switcher .is-choice { opacity: .85; }
.status-switcher .is-choice:hover { opacity: 1; }
.status-switcher .is-current { border-color: currentColor; }

/* ---------- Filters ---------- */
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.filters a {
  padding: 5px 12px; border-radius: 999px; font-size: 13px;
  background: var(--panel); border: 1px solid var(--line); color: var(--muted);
}
.filters a.active { background: var(--ink); border-color: var(--ink); color: var(--ink-contrast); }
.filters a:hover { text-decoration: none; border-color: var(--accent); }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--control-h); padding: 0 14px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--panel); color: var(--ink);
  font-size: 14px; font-family: inherit; cursor: pointer; text-decoration: none;
}
.btn:hover { border-color: var(--accent); text-decoration: none; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { filter: brightness(1.08); }
/* ============================================================
   THE ONE BUTTON A CLIENT CAME HERE TO PRESS.

   Soundcheck exists so a couple can tell Joel what they want
   played. Everything else on their page - the date, the venue,
   the times - is context for that one action, and the button
   was drawn like every other button on it, so it read as one
   more thing among several rather than as the thing.

   BLUE, BIG, AND FULL WIDTH ON A PHONE. Not the app's accent:
   that indigo is Joel's, it is on every button on his side, and
   a client has never seen it mean anything in particular. This
   is a colour that appears once on the page.
   ============================================================ */
.btn-plan {
  background: var(--plan-blue); border-color: var(--plan-blue);
  color: var(--plan-blue-ink);
  font-size: 16px; font-weight: 600;
  padding: 11px 20px;
  box-shadow: 0 2px 10px var(--shadow);
}
.btn-plan:hover { filter: brightness(1.08); }

.btn-danger { color: var(--danger); border-color: var(--danger-line); background: var(--panel); }
.btn-danger:hover { background: var(--danger); border-color: var(--danger); color: var(--ink-contrast); }
.page-actions form { display: inline; }

.link-danger {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--danger); font-size: 12px; font-family: inherit;
}

/* ---------- Forms ---------- */
.form { max-width: 640px; display: flex; flex-direction: column; gap: 14px; }
.form label { display: flex; flex-direction: column; gap: 5px; }
.form label > span { font-size: 13px; font-weight: 600; }
.form label em { font-weight: 400; color: var(--muted); font-style: normal; }
.form input, .form select, .form textarea {
  padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: var(--input); color: var(--ink); width: 100%;
}
/* A DATE AND A TIME ARE SIZED TO WHAT GOES IN THEM, EVERYWHERE THEY APPEAR.
   The rule above hands every field width: 100%, which is right for a name and
   absurd for "9:30 PM". A cap rather than a fixed width, so the box still
   shrinks on a narrow phone instead of pushing the form sideways, and so the
   label above it goes on being as wide as the form.

   FOUR SHAPES, ONE SIZE EACH. The browser's own date control on the gig form;
   the plain typable box the planning form uses; and the two wrappers app.js
   builds around them when it hangs a calendar or a time list off the side.
   The wrapper is what gets capped in that case - the input inside it is
   flex: 1 1 auto and fills whatever it is given. */
.form input[type="date"],
.form input[data-date-input],
.form .datebox,
.plan-question input[data-date-input],
.plan-question .datebox,
.inline-date input,
/* The wrapper app.js builds around the inquiry date on the gig page. Without
   it the cap lands on the input, which .datebox > input below then removes -
   and the box grows to whatever the browser feels like, which on a phone was
   wider than the page. */
.inline-date .datebox,
.deposit-state input[type="date"] { max-width: var(--field-date); }

.form input[type="time"],
.form input[data-time-input],
.form .timebox,
.plan-question input[data-time-input],
.plan-question .timebox { max-width: var(--field-time); }

/* And inside the pair of wrappers, the input is the whole width of it again -
   the cap belongs to the wrapper, or the panel hanging off it would be capped
   with it. */
.datebox > input, .timebox > input { max-width: none; }

.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; }
.form-actions { display: flex; gap: 8px; align-items: center; margin-top: 4px; }
.field-hint { margin: -6px 0 0; }
.field-hint code {
  background: var(--code-bg); padding: 1px 5px; border-radius: 4px; font-size: 11px;
}

/* A heading INSIDE a form, marking off a group of related fields. Sized to
   sit under the field labels around it rather than shout over them. */
.form-heading {
  margin: 10px 0 -4px; font-size: 13px; font-weight: 700;
  padding-bottom: 6px; border-bottom: 1px solid var(--line);
}

/* A tick and its words, side by side - every other label in a form stacks
   its words above its box, which for a checkbox puts them in the wrong
   order and leaves the tick stretched across the whole width. */
/* Higher specificity than ".form label" above, which would otherwise keep
   flex-direction: column and stack the tick on top of its own label. */
.form .check-row { flex-direction: row; align-items: center; gap: 8px; }
.check-row > span { font-weight: 600; }

/* THE BUG THIS FIXES, because it was the worst-looking thing in the app.

   The shared rule above gives every input in a form width: 100%. For a text
   box that is right. For a RADIO it stretched the control across the whole
   fieldset, which draws the dot in the MIDDLE and leaves the words stranded
   against the far edge - so on "Which events" no label appeared to belong to
   any button. Checkboxes were already excused; radios were not, and there
   was no reason for the difference.

   Written on the element rather than inside .check-row so that a radio added
   anywhere in any form tomorrow is right without anybody remembering this. */
.form input[type="checkbox"],
.form input[type="radio"] { width: auto; margin: 0; flex: 0 0 auto; }

/* The referral pair: a tick, and a name that only matters once it is on.
   Hiding the name box is done in CSS alone so it is right from the first
   paint and needs no JavaScript. A browser without :has() shows the box all
   the time, which is untidy and still works - and the server drops any name
   that arrives with the tick off either way. */
.referral { display: flex; flex-direction: column; gap: 10px; }
.referral:has(input[type="checkbox"]:not(:checked)) .referral-name,
.referral:has(input[type="checkbox"]:not(:checked)) .muted-sm { display: none; }

/* ============================================================
   THE PICKER: "ONE I HAVE USED BEFORE, OR A NEW ONE"

   Client, venue and referrer, all three. See
   views/partials/picker.ejs for what it is made of and
   public/app.js for what it becomes.

   WITH NO JAVASCRIPT this is two ordinary fields stacked - a
   dropdown and a text box - and everything below styles the
   thing app.js builds in their place. .is-live is what says
   that happened, so nothing here can leave the plain version
   looking half-built.
   ============================================================ */

.picker { display: flex; flex-direction: column; gap: 5px; }
.picker .picker-existing, .picker .picker-new { gap: 5px; }
.picker-hint { margin: 2px 0 0; }
.picker-heading { font-size: 13px; font-weight: 600; }

.picker-box { position: relative; }

/* IT HAS TO LOOK LIKE SOMETHING YOU CAN TYPE IN, which is exactly what the old
   referrer field got wrong: it accepted typing and looked like a dropdown, so
   nobody ever typed. A text cursor, a text box's border, and no chevron
   pretending the only way in is a click at one end. */
.picker-input {
  width: 100%; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: var(--input); color: var(--ink);
  cursor: text;
}

/* The list. Sized so a long list scrolls rather than running off the page,
   and over everything below it rather than pushing the form about. */
.picker-panel {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40;
  margin: 0; padding: 4px; list-style: none;
  max-height: 264px; overflow-y: auto; overscroll-behavior: contain;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 8px; box-shadow: 0 10px 30px var(--shadow);
}
.picker-panel[hidden] { display: none; }

.picker-row {
  padding: 7px 10px; border-radius: 6px; cursor: pointer;
  overflow-wrap: anywhere;
}
.picker-row:hover { background: var(--focus-field); }
/* The row the keyboard is on. The accent block, the same as the time list, so
   "where am I" reads the same in both. */
.picker-row.is-on { background: var(--accent); color: var(--accent-ink); }

/* "None of them" is a way back to blank rather than a thing on the list, and
   the last row is a thing to make rather than a thing to pick. Both are said
   quietly, so the real choices are what the eye lands on. */
.picker-row.is-none { color: var(--muted); }
.picker-row.is-none.is-on { color: var(--accent-ink); }
.picker-row.is-new {
  border-top: 1px solid var(--line); border-radius: 0 0 6px 6px;
  margin-top: 4px; padding-top: 10px; font-weight: 600;
}
.picker-empty { padding: 8px 10px; color: var(--muted); }

/* ---------- Venues ---------- */

/* "Where were the old gigs?" - two answers, side by side, neither of them a
   default. Each button carries its own sentence underneath saying what IT
   will do, because that is the thing to read. See views/venues/moved.ejs. */
.moved-choices {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px; margin: 18px 0;
}
.moved-choices form {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 14px; border: 1px solid var(--line); border-radius: var(--radius);
}
.moved-choices p { margin: 0; }

/* DELETE, AND DON'T. Side by side, and wrapping onto two lines on a phone
   rather than squeezing - the one thing that must never happen here is the
   two of them ending up close enough together to hit the wrong one. Both are
   ordinary buttons: the words say which is which, and the alarm colour on the
   first is the second signal rather than the only one. */
.delete-choices {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  margin: 18px 0 6px;
}
.delete-choices form { margin: 0; }

/* A short list of records with no table around it. */
.plain-list { margin: 12px 0; padding-left: 20px; }
.plain-list li { margin-bottom: 4px; }
.plain-list .muted-sm { margin-left: 6px; }

/* ---------- Fixing a referrer's name, on the reporting page ---------- */
/* Folded away by default: it is a thing Joel does twice a year, and the table
   above it is what the page is for. See the note in views/reports.ejs. */
.referrer-fix { margin-top: 18px; }
.referrer-fix summary {
  cursor: pointer; font-size: 13px; font-weight: 600; color: var(--muted);
}
.referrer-fix summary:hover { color: var(--ink); }
.referrer-fix[open] summary { margin-bottom: 8px; }
.referrer-fix-forms {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px; margin-top: 12px;
}
.referrer-fix-forms form {
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
  padding: 14px; border: 1px solid var(--line); border-radius: var(--radius);
}
.referrer-fix-forms h3 { margin: 0; font-size: 14px; }
.referrer-fix-forms label { display: flex; flex-direction: column; gap: 4px; width: 100%; }
.referrer-fix-forms label > span { font-size: 13px; font-weight: 600; }
.referrer-fix-forms select, .referrer-fix-forms input {
  width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: var(--input); color: var(--ink);
}

/* ---------- Preferences menu (in the top bar) ---------- */
.pref-menu { position: relative; }
/* Square with the same rounded corners, the same height and the same border
   as "+ Gig" and "+ Client", so the three read as one set of buttons rather
   than two buttons and a badge. It is as wide as it is tall because it holds
   an icon and no words. */
.pref-menu > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--control-h); height: var(--control-h); padding: 0;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel); color: var(--ink);
}
.pref-menu > summary::-webkit-details-marker { display: none; }
.pref-menu > summary:hover { border-color: var(--accent); color: var(--accent); }
.pref-menu[open] > summary { border-color: var(--accent); color: var(--accent); }
/* Drawn in views/partials/preferences-menu.ejs. Sized here so it fills the
   button rather than rattling around inside it. */
.pref-icon { display: block; width: 20px; height: 20px; }

.pref-panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
  width: 260px; padding: 14px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 10px 30px var(--shadow);
  display: flex; flex-direction: column; gap: 14px;
}
.pref-group { border: none; padding: 0; margin: 0; }
.pref-group legend {
  padding: 0 0 6px; font-size: 11px; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
}
.pref-choice {
  display: flex; flex-direction: row; align-items: flex-start; gap: 9px;
  padding: 6px 8px; border-radius: 7px; cursor: pointer; font-size: 14px;
}
.pref-choice:hover { background: var(--input); }
.pref-choice input { width: auto; margin: 3px 0 0; accent-color: var(--accent); }
.pref-choice .muted-sm { margin-top: 1px; }
.pref-save { margin-top: 4px; }

/* ---------- Search (in the top bar, and its results page) ---------- */
.site-search {
  position: relative;
  flex: 1 1 200px;
  max-width: 340px;
  min-width: 0;
  margin: 0;
}
.site-search-input {
  width: 100%;
  /* Exactly as tall as "+ Gig" and the sliders button, so the top bar reads
     as one row of controls rather than three heights side by side. */
  height: var(--control-h); padding: 0 14px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--input); color: var(--ink);
  font: inherit; font-size: 14px;
}
.site-search-input::placeholder { color: var(--muted); }
.site-search-input:hover { border-color: var(--accent); }
/* Focus is the shared field treatment near the top of this file - a lifted
   border and a lifted background, no ring. */

/* The panel under the box. Built by public/app.js; without it the box is
   still a form and Enter still goes to the results page. */
.site-search-panel {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 30;
  max-height: min(70vh, 460px); overflow: auto;
  padding: 6px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 10px 30px var(--shadow);
}
.site-search-panel[hidden] { display: none; }
.search-group + .search-group { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px; }
.search-group-label {
  margin: 2px 0 4px; padding: 0 8px;
  font-size: 11px; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
}
.search-result {
  display: block; padding: 6px 8px; border-radius: 7px;
  color: var(--ink); font-size: 14px; line-height: 1.35;
}
.search-result:hover { text-decoration: none; background: var(--input); }
/* Where the arrow keys are. Deliberately louder than hover: this one is
   what Enter opens. */
.search-result.is-active { background: var(--accent); color: var(--accent-ink); }
.search-result.is-active .muted-sm { color: var(--accent-ink); opacity: .85; }
.search-result-title { display: block; font-weight: 600; }
.search-all { font-weight: 600; color: var(--accent); }
.search-all.is-active { color: var(--accent-ink); }
.search-more, .search-empty {
  margin: 2px 0 4px; padding: 2px 8px 4px;
  font-size: 12px; color: var(--muted);
}

/* The results page uses the same two lines per row as the dropdown. */
.search-list { list-style: none; margin: 0; padding: 0; }
.search-list li { padding: 9px 0; border-top: 1px solid var(--line); }
.search-list li:first-child { border-top: none; }

/* ---------- Sortable table headers ---------- */
/* Built by public/app.js out of a plain <th>. Without JavaScript the heading
   stays ordinary text - not a button that does nothing. */
.sort-link {
  display: inline-flex; align-items: center; gap: 5px;
  border: none; background: none; padding: 0; margin: 0;
  font-family: inherit; cursor: pointer;
  color: var(--muted); text-transform: uppercase;
  letter-spacing: .05em; font-size: 11px; font-weight: 700;
}
.sort-link:hover { color: var(--ink); text-decoration: none; }
.sort-link.is-sorted { color: var(--accent); }
.sort-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }
/* Reserve the arrow's width so a heading doesn't shift when it gains one. */
.sort-arrow { font-size: 9px; line-height: 1; min-width: 8px; }

/* ---------- The line between past and upcoming ---------- */
.divider-row td {
  padding: 0; border-bottom: none;
  background:
    linear-gradient(var(--accent), var(--accent)) center / 100% 1px no-repeat;
}
.divider-label {
  display: inline-block; margin: 10px 0 10px 10px;
  padding: 2px 10px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink);
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em;
}

/* ---------- Pop-up gig lists ---------- */
.gig-dialog {
  width: min(920px, calc(100vw - 32px));
  max-height: min(80vh, 760px);
  padding: 0; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); color: var(--ink);
  box-shadow: 0 24px 60px var(--shadow);
  overflow: hidden;
}
.gig-dialog::backdrop { background: var(--backdrop); }
.dialog-head {
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  padding: 14px 18px; border-bottom: 1px solid var(--line);
  position: sticky; top: 0; background: var(--panel);
}
.dialog-head h2 { margin: 0; }
.dialog-body { overflow: auto; max-height: calc(80vh - 118px); padding: 0 18px; }
.dialog-foot {
  margin: 0; padding: 10px 18px 14px; border-top: 1px solid var(--line);
}

/* ---------- Dashboard stat heading + year chip ---------- */
.stats-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 16px; margin: 18px 0 10px;
}
.stats-head h2 { margin: 0; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--accent) !important; border-color: var(--accent) !important;
  color: var(--accent-ink) !important;
}

/* ---------- Phone numbers ---------- */
/*
   Plain text until public/app.js says it can be copied, so it never looks
   clickable when clicking it would do nothing.

   NO UNDERLINE, of either kind. A dotted one is the "hover me for a
   definition" gesture and this is not a definition; a solid one is a link
   and this does not go anywhere. What it does is copy, so what it shows is
   a tooltip that says Copy - and then a tick once it has.
*/
.phone { white-space: nowrap; }
.phone[data-copy-ready] { cursor: pointer; border-radius: 3px; }
.phone[data-copy-ready]:hover { color: var(--accent); }

/* The bubble's own space, beside the number and always there whether or not
   anything is in it. Reserving it is what stops the table twitching every
   time a tooltip appears. */
.copy-hint {
  position: relative; display: inline-block;
  width: 48px; height: 1em; vertical-align: -0.15em;
}
.copy-bubble {
  position: absolute; left: 6px; top: 50%; transform: translateY(-50%);
  white-space: nowrap; pointer-events: none;
  padding: 1px 7px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--input); color: var(--muted);
  font-size: 11px; font-weight: 600; letter-spacing: .03em;
  opacity: 0; transition: opacity .12s ease;
}
/* Half a second of hovering before it appears, so it doesn't flash at every
   mouse that happens to cross the number on its way somewhere else. Moving
   away hides it at once. Arriving by keyboard shows it straight away: there
   is no such thing as tabbing past something by accident. */
.phone[data-copy-ready]:hover + .copy-hint .copy-bubble { opacity: 1; transition-delay: .5s; }
.phone[data-copy-ready]:focus-visible + .copy-hint .copy-bubble { opacity: 1; transition-delay: 0s; }
/* Copied. The tooltip becomes a tick in the same place - no waiting, and no
   second thing appearing anywhere else - and takes itself away after a
   couple of seconds. Same weight of selector as the hover rules above, and
   after them, so it wins while the mouse is still sitting on the number. */
.phone[data-copy-ready] + .copy-hint.is-flash .copy-bubble {
  opacity: 1; transition-delay: 0s;
  background: var(--notice-bg); border-color: var(--notice-line); color: var(--notice-ink);
}

/* Present for screen readers, invisible on screen. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.error {
  background: var(--error-bg); border: 1px solid var(--error-line); color: var(--error-ink);
  padding: 10px 14px; border-radius: 8px; max-width: 640px;
}
.error p { margin: 4px 0; }

.notice {
  background: var(--notice-bg); border: 1px solid var(--notice-line); color: var(--notice-ink);
  padding: 10px 14px; border-radius: 8px; max-width: 640px;
}

/* ============================================================
   "THAT WORKED.", IN WHICHEVER OF THREE WAYS FITS.

   The tiering lives in src/flash.js and the essay at the top
   of it says why. This is the drawing of it, and there are
   two things drawn:

     .flash-disc     - tier two. A filled disc floating in the
                       middle of the window with the mark
                       knocked out in white. It is what an
                       action gets when its RESULT IS NOT ON
                       SCREEN: an invite, a sign-in link, a
                       save on a page being left behind.
     [data-settled]  - tier one. The row that just changed,
                       highlighting and settling. No floating
                       anything: the row IS the confirmation.

   WHY THE DISC REPLACED A BARE TICK. A check mark drawn
   straight onto the page has no edges. Over a heading or a
   dense table the strokes run into the words behind them and
   the eye has to sort out which belong to which. A solid disc
   has a boundary; the mark inside it is white on one known
   colour, measured in test/contrast.test.js, and legible over
   anything at all.

   EVERY LINE BELOW IS DOING ONE OF FIVE JOBS:

     position: fixed    - it is not in the layout, so it cannot
                          move the page by a pixel, and it is
                          where the eye is whatever the scroll
                          position is.
     pointer-events     - none, on the layer AND on the disc.
                          A click aimed at what is under it
                          reaches what is under it. There is
                          nothing to dismiss and nothing to hit.
     the animation      - the disc scales in, the mark strokes
                          on behind it, it holds, it fades. All
                          CSS, no JavaScript, which is the same
                          standard everything else here is held
                          to, and it ends at opacity 0 and
                          visibility hidden so nothing lingers.
     the colours        - variables, so both themes are right
                          with one copy of everything.
     reduced motion     - at the bottom. Nothing draws, nothing
                          scales, nothing fades: two states with
                          nothing in between.
   ============================================================ */

.flash-mark {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px;
  /* THE WHOLE LAYER IS UNTOUCHABLE. Not decoration - it is over the middle of
     the window at the exact moment somebody is likely to click something, so
     a layer that swallowed one press would be worse than no confirmation. */
  pointer-events: none;
}

/* The disc. Sized in vmin so it is a big mark on a laptop and a big mark on a
   phone, rather than a big mark on one and a speck on the other - and capped,
   so it does not become absurd on a very wide screen. */
.flash-disc {
  display: block; pointer-events: none;
  width: min(26vmin, 168px); height: min(26vmin, 168px);
}
.flash-disc svg { display: block; width: 100%; height: 100%; overflow: visible; }

.flash-disc-face {
  fill: var(--ok-disc);
  /* A shadow rather than a border: it lifts the disc off whatever is behind
     it without drawing a second edge inside its own outline. */
  filter: drop-shadow(0 10px 28px var(--shadow));
  transform-origin: 48px 48px;
}
.is-problem .flash-disc-face { fill: var(--bad-disc); }

/* The check, or the cross, knocked out of the disc in white. Round caps and
   joins, because this is a mark somebody made rather than a diagram. */
.flash-disc-stroke {
  fill: none; stroke: var(--disc-ink); stroke-width: 9;
  stroke-linecap: round; stroke-linejoin: round;
  /* Long enough to cover either path outright. The exact length does not
     matter as long as it is not short: what is being animated is the offset
     going to zero, and a dash longer than the line is one unbroken line. */
  stroke-dasharray: 160; stroke-dashoffset: 160;
}

/* Appear, hold, fade, gone - the layer as a whole. The disc is brief: it is a
   glance, not a read. The sentence is held long enough to actually be read. */
@keyframes flash-in-out {
  0%   { opacity: 0; }
  8%   { opacity: 1; }
  55%  { opacity: 1; }
  100% { opacity: 0; visibility: hidden; }
}

/* The disc arriving. Slightly past full size and back, so it lands rather
   than simply being there. */
@keyframes flash-disc-in {
  0%   { transform: scale(.3); }
  60%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

/* And the mark drawn onto it, a stroke at a time. */
@keyframes flash-stroke-on {
  to { stroke-dashoffset: 0; }
}

.flash-mark[data-flash="done"] { animation: flash-in-out 1400ms ease-out forwards; }
.flash-mark[data-flash="problem"] { animation: flash-in-out 6s ease-out forwards; }

.flash-mark .flash-disc-face {
  animation: flash-disc-in 320ms cubic-bezier(.2, .9, .3, 1.2) both;
}
.flash-mark .flash-disc-stroke {
  /* After the disc has arrived, not with it: the disc is the surface and the
     mark is put onto it. */
  animation: flash-stroke-on 260ms ease-out 240ms forwards;
}
/* The second half of the cross follows the first, so it reads as two strokes
   rather than as an X appearing. */
.flash-mark .flash-disc-stroke.is-second { animation-delay: 420ms; }

/* A FAILURE IS WORDS. Same floating, same non-blocking, same taking itself
   away - and a sentence under the disc, because a mark can say that something
   went wrong and can never say what. This one gets a panel: it is text, and
   text over a table is unreadable without one. */
.flash-said {
  margin: 0; max-width: min(90vw, 460px); padding: 14px 18px;
  border: 1px solid var(--error-line); border-radius: var(--radius);
  background: var(--error-bg); color: var(--error-ink);
  box-shadow: 0 12px 40px var(--shadow);
  font-size: 16px; text-align: center;
}

/* ============================================================
   TIER ONE: THE ROW SAYS SO ITSELF.

   No floating anything. The row Joel just acted on brightens
   and settles back, and the parts of it that changed - a
   "retired" badge arriving, a button's label becoming its
   opposite - fade in rather than snapping into place.

   THIS IS THE CONFIRMATION, not decoration on top of one.
   These actions used to change instantly and silently, which
   is precisely why they did not register as having happened:
   the page came back looking almost the same and there was
   nothing to catch the eye. Motion is what says "this, here,
   just now".

   The server puts data-settled on exactly one element per page
   load - see changedHere() in src/flash.js - so this is a
   thing that happens once, to one row, and never during
   ordinary reading.
   ============================================================ */

/* THERE IS NO 100% KEYFRAME, AND THAT IS THE WHOLE TRICK. A rule that ended
   at a colour would have to name one, and the rows this is put on do not
   agree about what theirs is: a question in a list has no background at all,
   the card around it is --panel, and a line in the details grid is different
   again. CSS fills in the missing final keyframe with the ELEMENT'S OWN
   value, whatever that happens to be - so the highlight fades back into the
   row rather than into a guess about it, and there is no fill mode leaving a
   card permanently transparent afterwards. */
@keyframes settle-row {
  0%   { background-color: var(--settle); }
  35%  { background-color: var(--settle); }
}

[data-settled] {
  animation: settle-row 1100ms ease-out;
  /* So the highlight has an edge to fill rather than bleeding into the row
     above and below. */
  border-radius: var(--radius);
}

/* The bits of the row whose WORDS changed. A short crossfade, so the eye is
   drawn to the label that is different rather than being asked to notice that
   it always was. */
@keyframes settle-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

[data-settled] .settles { animation: settle-in 420ms ease-out 120ms both; }

/* ============================================================
   AND NONE OF IT FOR SOMEBODY WHO ASKED FOR LESS MOVEMENT.

   Both tiers. The disc is there and then it is not, with no
   scaling and no drawing; the row highlights and settles in
   one step rather than over a second. The information is
   identical - what is removed is the movement, which is what
   was asked for and all that was asked for.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  @keyframes flash-in-out {
    0%   { opacity: 1; }
    99%  { opacity: 1; }
    100% { opacity: 0; visibility: hidden; }
  }
  .flash-mark .flash-disc-face { animation: none; }
  .flash-mark .flash-disc-stroke { animation: none; stroke-dashoffset: 0; }

  /* The row still marks itself - it is the only confirmation these actions
     get, so taking it away entirely would leave nothing at all. What goes is
     the fade: it holds, then it is done. */
  @keyframes settle-row {
    0%   { background-color: var(--settle); }
    99%  { background-color: var(--settle); }
  }
  [data-settled] .settles { animation: none; }
}

/* The one button that lets a client into Soundcheck, and the line that says
   what it will do. Wraps under the button on a phone rather than squeezing
   the sentence into a column three words wide. */
.invite-form {
  display: flex; align-items: center; gap: 10px 14px; flex-wrap: wrap;
  margin-top: 14px;
}

/* ---------- Details list ---------- */
/* THE SECOND COLUMN IS minmax(0, 1fr) AND NOT 1fr, which is not a detail. A
   grid track defaults to a minimum of min-content, so `1fr` refuses to shrink
   below the widest unbreakable thing in it - and on a phone that pushed the
   whole page sideways, giving every gig a horizontal scrollbar. minmax(0, ...)
   is what lets the text wrap instead. The label column shrinks a little on the
   way down for the same reason. (This was already true before the inquiry date
   moved onto this list; the new row made it easier to notice.) */
.details {
  display: grid; grid-template-columns: minmax(96px, 150px) minmax(0, 1fr);
  gap: 8px 16px; margin: 0;
}
.details dt { font-size: 13px; color: var(--muted); }
.details dd { margin: 0; }

.deposit-state .deposit-form {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 6px;
}
/* An editable line inside the details list - the inquiry date, beside the
   deposit, which is the same shape of thing: a fact that arrives at a
   different moment from the booking. How wide the box is lives with the rest
   of the date sizing, up beside --field-date. */
.inline-date form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* min-width: 0 because a flex item will not shrink below its own min-content
   otherwise, and a text input's min-content is however many characters the
   browser feels a text input should be - about twenty. On a phone that made
   the box wider than the column it was in, and the page scrolled sideways. */
.inline-date form > * { min-width: 0; }
.inline-date input, .deposit-state input[type="date"] {
  padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: var(--input); color: var(--ink);
}

/* ---------- Notes ---------- */
.note-form { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; margin-bottom: 16px; }
.note-form textarea {
  width: 100%; padding: 9px 11px; border: 1px solid var(--line);
  border-radius: 8px; font: inherit; resize: vertical;
  background: var(--input); color: var(--ink);
}

.note-list { list-style: none; margin: 0; padding: 0; }
.note-list li { padding: 12px 0; border-top: 1px solid var(--line); }
.note-list p { margin: 0 0 6px; white-space: pre-wrap; }
.note-meta { display: flex; gap: 12px; align-items: center; font-size: 12px; color: var(--muted); }
.note-meta form { display: inline; }

/* ---------- Footer ---------- */
.site-footer {
  max-width: 1060px; margin: 0 auto; padding: 8px 24px 40px;
  font-size: 12px; color: var(--muted);
}
.site-footer code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }

/* A FILE PATH IS ONE LONG WORD WITH NO SPACES IN IT, and a browser will not
   break a word unless it is told it may. On a server these are long -
   /var/data/dj-crm.sqlite and the folder the backups sit in - and one of them
   sitting in the footer of every page was enough on its own to push a phone
   sideways, which is the exact thing five other fixes in this file exist to
   stop. "anywhere" rather than "break-word" because a path has no sensible
   break point and mid-word is the only place left. */
.site-footer code, .card code { overflow-wrap: anywhere; }

/* ============================================================
   REPORTING PAGE
   Every colour below is one of the variables at the top of this
   file, which is what makes the charts right in both themes
   without a second copy of anything. There are no new palette
   entries here on purpose: a chart that invented its own blue
   would be the one thing on the page that stopped matching when
   the theme changed.
   ============================================================ */

/* Which range the whole page is showing, said in words. Loud enough that a
   screenshot of this page can't be mistaken for a different range. */
.range-statement {
  margin: 0 0 20px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--panel);
  font-size: 14px;
}

/* A heading inside a card, under the card's own <h2>. */
.sub-heading {
  margin: 24px 0 10px;
  font-size: 13px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink);
}
.card > .sub-heading:first-of-type { margin-top: 18px; }

.table-caption {
  caption-side: top; text-align: left;
  padding: 0 0 10px;
  font-size: 12px; color: var(--muted);
}

/* ---------- A TABLE THAT WILL NOT FIT, AND THE PAGE THAT MUST NOT MOVE ----------

   Eight columns of dates, names and money do not fit in 375 pixels, and no
   amount of shrinking makes them: every column says something, and a gig list
   with the fee cut off is a gig list nobody can use.

   So the table is allowed to be as wide as it needs to be, and this is the box
   it does that INSIDE. Without it, the widest table on a page pushes the whole
   document sideways - the heading, the top bar, the buttons, everything - and
   the only way back to the left edge is a scrollbar at the very bottom of the
   page, underneath the thing you were trying to read. With it, the page stays
   exactly 375 wide and one card scrolls under your thumb, which is a gesture
   that needs no explaining on a phone.

   EVERY TABLE IN THIS APP THAT CAN OUTGROW A PHONE IS IN ONE OF THESE. The
   dashboard, the gig list, the client list, a client's own page and the
   reports all wrap theirs; test/appearance.test.js walks the templates and
   fails if a new one turns up that doesn't.

   position: relative IS NOT DECORATION AND MUST NOT BE REMOVED. Without it
   the wrapper does not fully work, in a way that takes an afternoon to find:
   an element inside the table that is position:absolute - and app.js puts one
   in every sortable column heading, the hidden line that tells a screen reader
   which way the sort is pointing - has its position worked out against the
   PAGE rather than against this box. Something positioned against the page is
   not clipped by this box's overflow, so it went on pushing the document
   sideways exactly as before, while every element in the layout measured a
   perfectly innocent 375 pixels wide. This line makes the wrapper the thing
   those are positioned against, which is what puts them inside the scroll it
   is creating. It is why the venues table looked fixed and the gig list did
   not: the venues table has no sortable headings in it. */
.table-wrap { position: relative; overflow-x: auto; }

/* ---------- Earned and expected, told apart ----------
   The two are never one figure on this page, and where they sit side by side
   the tile itself carries the same solid-versus-hatched distinction the charts
   use. The bar down the left is not the only difference - each tile's own
   label says which it is - it just makes the pair scannable. */
.stat-earned  { border-left: 3px solid var(--accent); }
.stat-expected {
  border-left: 3px solid var(--accent);
  border-left-style: dashed;
}

/* ---------- Charts ---------- */
.chart-frame { margin: 4px 0 12px; }
/* Scaled to the width it is given, keeping its own proportions - so the text
   and the hairlines inside never stretch. */
.chart { display: block; width: 100%; height: auto; }

.chart-gridline { stroke: var(--line); stroke-width: 1; }
.chart-baseline { stroke: var(--muted); stroke-width: 1; }
.chart-axis { fill: var(--muted); font-size: 10px;
              font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
.chart-axis-year { font-weight: 700; }

/* EARNED is solid. Expected is not given a fill here on purpose: it is filled
   with one of the hatch patterns defined in views/partials/chart.ejs, and a
   fill in this file would override that pattern, because a CSS rule always
   beats an attribute on the element. Only the outline is set here. */
.ch-current-earned { fill: var(--accent); }
.ch-prior-earned   { fill: var(--muted); }
.ch-current-expected { stroke: var(--accent); stroke-width: .75; }
.ch-prior-expected   { stroke: var(--muted);  stroke-width: .75; }

/* The stripes themselves, inside the patterns. */
.ch-hatch-bg      { fill: var(--panel); }
.ch-hatch-current { stroke: var(--accent); }
.ch-hatch-prior   { stroke: var(--muted); }

/* ---------- Chart legend ----------
   Colour is never the only thing separating two series on this page - this
   period is the left bar and a year earlier the right one, earned is solid and
   expected is hatched - but the legend still has to say which is which in
   words, because a swatch alone explains nothing. */
.chart-legend {
  list-style: none; margin: 0 0 10px; padding: 0;
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  font-size: 12px; color: var(--muted);
}
.chart-legend li { display: flex; align-items: center; gap: 7px; }
.swatch {
  width: 14px; height: 14px; border-radius: 3px; flex: none;
  border: 1px solid transparent;
}
.sw-current-earned { background: var(--accent); }
.sw-prior-earned   { background: var(--muted); }
/* The same 45-degree stripes as the pattern in the chart, in CSS, so the key
   and the thing it is keying look alike. */
.sw-current-expected {
  border-color: var(--accent);
  background: repeating-linear-gradient(
    45deg, var(--accent) 0 3px, var(--panel) 3px 6px);
}
.sw-prior-expected {
  border-color: var(--muted);
  background: repeating-linear-gradient(
    45deg, var(--muted) 0 3px, var(--panel) 3px 6px);
}

/* ---------- The chart's numbers, as a table ----------
   Closed by default so it doesn't crowd the page, and a real <details>, so it
   opens with no JavaScript and its contents are in the page for a screen
   reader whether or not anybody opens it. */
.chart-data { margin-top: 14px; }
.chart-data > summary {
  cursor: pointer; font-size: 12px; font-weight: 700;
  color: var(--muted); text-transform: uppercase; letter-spacing: .05em;
  padding: 6px 0;
}
.chart-data > summary:hover { color: var(--ink); }
.chart-data[open] > summary { margin-bottom: 6px; }
.chart-data table { font-size: 13px; }

/* ---------- Average fee, with a bar beside the number ----------
   Beside, never instead of. The figure is the answer; the bar only saves
   comparing twenty of them by eye, so it is aria-hidden and carries no
   information the number doesn't already have. */
.col-avg { text-align: right; white-space: nowrap; }
th.col-avg { text-align: right; }
.avg-value { display: block; }
.avg-bar {
  display: block; width: 100%; max-width: 90px; height: 4px; margin: 4px 0 0 auto;
  background: var(--input); border-radius: 999px; overflow: hidden;
}
.avg-bar-fill { display: block; height: 100%; background: var(--accent); }

.col-excluded { font-size: 12px; white-space: nowrap; }

/* "Highest average fee" - the one row a revenue ranking would otherwise
   bury. */
.tag {
  display: inline-block; margin-left: 6px; padding: 1px 8px;
  border-radius: 999px; border: 1px solid var(--accent);
  color: var(--accent); background: transparent;
  font-size: 11px; font-weight: 600; white-space: nowrap;
}

/* A month with nothing booked in it. Shaded, not hidden: an empty month is the
   thing the pipeline section exists to show you early.

   --code-bg rather than --input, which would be invisible here: in the light
   theme a field and a card are both plain white, so tinting the row with
   --input tinted it with the colour it was already sitting on. */
.row-thin td { background: var(--code-bg); }

/* The year-on-year sentence above the revenue chart. */
.compare-line { margin: 0 0 14px; font-size: 14px; }

/* ============================================================
   SIGNING IN, AND SOUNDCHECK
   The client's side of the app.

   EVERY COLOUR BELOW IS ONE OF THE VARIABLES AT THE TOP OF THIS
   FILE. There is not a single new hex value here, which is what
   makes the portal correct in dark and light without a second
   copy of anything - and it is why the contrast test already
   covers these pages: they are drawn from pairs it already
   measures.

   What makes this read as the client's area rather than Joel's
   is not colour. It is space: wider margins, fewer things per
   screen, and lists instead of tables. Joel's side is a tool
   he uses fifty times a day; this is a page somebody opens
   twice a month and wants to feel calm about.
   ============================================================ */

/* ---------- The sign-in page ---------- */
/* Narrower than the app's normal column, and pushed down the screen. A single
   short form centred in a wide page looks abandoned. */
.signin { max-width: 460px; margin: 6vh auto 0; }
.signin-brand {
  margin: 0 0 6px;
  font-size: 30px; letter-spacing: -.01em;
}
.signin-sub { margin: 0 0 22px; color: var(--muted); }
.signin-form { max-width: none; }
.signin .error { max-width: none; margin-bottom: 16px; }
.signin .card p:last-child { margin-bottom: 0; }

/* The portal header carries a name and a way out, and nothing else, so it has
   no middle to fill. */
.site-header-portal .header-actions { margin-left: auto; align-items: center; }
.whoami { color: var(--muted); font-size: 14px; }

/* ---------- A client's list of events ----------
   A list, deliberately, where Joel's side of the app uses a table. A table is
   right for fifty gigs you are scanning down; two events belonging to one
   person are not a dataset, and drawing them in ruled rows with column
   headings makes somebody's wedding look like a spreadsheet entry. */
.event-list { list-style: none; margin: 0; padding: 0; }
.event {
  display: flex; flex-wrap: wrap; gap: 4px 28px;
  padding: 14px 0; border-top: 1px solid var(--line);
}
.event:first-child { border-top: none; padding-top: 4px; }
/* The date column is fixed-width so several events line up down the page,
   and allowed to shrink on a phone rather than forcing a sideways scroll. */
.event-when { flex: 0 1 190px; min-width: 150px; }
.event-what { flex: 1 1 220px; }
.event strong { display: block; font-weight: 600; }

.past-events { margin-top: 16px; }
.past-events > summary {
  cursor: pointer; padding: 6px 0;
  font-size: 12px; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
}
.past-events > summary:hover { color: var(--ink); }
.past-events[open] > summary { margin-bottom: 4px; }

/* ---------- "This part is coming" ----------
   The notice colours rather than the error ones: nothing is wrong here, and
   an empty card with no explanation is what would look like something being
   broken. */
.coming-soon {
  margin: 0;
  padding: 12px 14px;
  border: 1px solid var(--notice-line); border-radius: 8px;
  background: var(--notice-bg); color: var(--notice-ink);
}

/* ---------- "That's somebody you already have" ----------
   Neither an error nor a confirmation, and it should look like neither. Red
   would tell Joel he did something wrong, and he didn't - he typed the person
   he meant. Green is the app's "that worked", and nothing has been saved.

   So it borrows the reporting page's .range-statement treatment instead: a
   plain panel with an accent rule down the side, which is what this app
   already uses to say "here is a fact about what you are looking at". */
.match-notice {
  margin: 0 0 16px;
  padding: 10px 14px;
  max-width: 640px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--panel);
}

/* ============================================================
   DEVELOPMENT ONLY - "you are not really this client"
   Drawn by views/partials/header.ejs and views/dev/view-as.ejs,
   and only ever on Joel's own machine. On a real deployment
   nothing in this section is ever written into a page.

   EVERY COLOUR HERE IS ONE OF THE VARIABLES AT THE TOP OF THIS
   FILE, so it is right in dark and in light with no second copy
   of anything - and the one pair it depends on, --error-ink on
   --error-bg, is already measured in test/contrast.test.js.
   ============================================================ */

/* ---------- The banner ----------
   It has one job: to be impossible to miss and impossible to mistake for
   part of Soundcheck. Three things do that at once, and no ordinary portal
   content does any of them:

     - the app's ERROR colours, which the client's side never otherwise uses;
     - capital letters on the flag, which nothing else on that side uses;
     - it STICKS to the top of the window, so scrolling down a long page of
       somebody else's details never leaves it behind.

   Sticky rather than fixed: fixed would sit on top of the first line of the
   page underneath it, and this belongs in the flow above the header. */
.view-as-banner {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px;
  margin: 0; padding: 10px 24px;
  background: var(--error-bg);
  border-bottom: 3px solid var(--error-line);
  color: var(--error-ink);
  font-size: 14px; line-height: 1.4;
}
.view-as-flag,
.dev-flag {
  flex: 0 0 auto;
  padding: 2px 8px; border-radius: 5px;
  border: 1px solid var(--error-line);
  font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .09em;
}
.view-as-said { flex: 1 1 260px; }
.view-as-banner form { flex: 0 0 auto; margin: 0; }
/* Shorter than a normal .btn - this is a bar, not a toolbar - and drawn in
   the banner's own ink so it reads as part of the warning rather than as a
   button that happened to land in it. */
.view-as-back,
.dev-signin-go {
  min-height: 30px; padding: 0 12px;
  background: transparent;
  border-color: var(--error-ink); color: var(--error-ink);
  font-weight: 600;
}
.view-as-back:hover,
.dev-signin-go:hover { background: var(--error-ink); color: var(--error-bg); border-color: var(--error-ink); }

/* ---------- The "View as…" button in Joel's top bar ----------
   A dashed edge, which nothing else in the app has. It says "this is not
   really part of the product" without needing a colour that would shout at
   him fifty times a day. */
.btn-dev { border-style: dashed; color: var(--muted); }
.btn-dev:hover { border-style: dashed; border-color: var(--accent); color: var(--ink); }

/* ---------- The list of people to be ---------- */
.view-as-warning {
  margin: 0 0 20px; padding: 10px 14px; max-width: 720px;
  border: 1px solid var(--error-line); border-radius: var(--radius);
  background: var(--error-bg); color: var(--error-ink);
  font-size: 14px;
}
.view-as-warning code {
  background: var(--code-bg); color: var(--ink);
  padding: 1px 5px; border-radius: 4px; font-size: 12px;
}

.view-as-list { list-style: none; margin: 0; padding: 0; }
.view-as-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 20px;
  padding: 12px 0; border-top: 1px solid var(--line);
}
.view-as-row:first-child { border-top: none; padding-top: 2px; }
.view-as-who { flex: 1 1 260px; }
.view-as-who strong { display: block; font-weight: 600; }
.view-as-row form { margin: 0; }
.view-as-current { color: var(--muted); font-size: 13px; }

/* ---------- "Sign in as Joel", on the sign-in page ----------
   Drawn by views/auth/signin.ejs, and only when the request came off this
   machine's own loopback interface in a development copy - the same question
   that decides everything else in this section.

   It is the same treatment as the banner above, for the same reason and with
   the same three parts: the app's ERROR colours, a capitalised DEVELOPMENT
   flag, and an outlined button drawn in the panel's own ink. The sign-in page
   is the first thing a real client ever sees, so a shortcut sitting on it has
   to read as "this is not part of the product" at a glance and from across
   the room.

   Set apart from the real form by a clear gap, and BELOW it: the form is what
   this page is for, and this is a thing that happens to be here too. */
.dev-signin {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px;
  margin: 22px 0 0; padding: 12px 14px;
  border: 1px solid var(--error-line); border-radius: var(--radius);
  background: var(--error-bg); color: var(--error-ink);
  font-size: 14px; line-height: 1.5;
}
/* The whole first line, with the flag sitting inline at the start of it, and
   the button on its own line underneath. The banner in the header puts all
   three side by side because it is as wide as the window; this panel is as
   wide as the sign-in form, which is not enough for three things without the
   words breaking into a narrow column. */
.dev-signin-said { flex: 1 1 100%; }
.dev-signin-said .dev-flag { margin-right: 8px; vertical-align: 2px; }
.dev-signin form { flex: 0 0 auto; margin: 0 0 0 auto; }

/* ============================================================
   THE PLANNING FORM
   Drawn by views/partials/plan-form.ejs, on three pages: the
   client's form in Soundcheck, Joel's copy on the gig page, and
   the preview in the question editor.

   EVERY COLOUR HERE IS ONE OF THE VARIABLES AT THE TOP OF THIS
   FILE. Not one new hex value, which is what keeps these pages
   right in dark and light without a second copy of anything -
   and what puts them inside test/contrast.test.js, which already
   measures the pairs they are built from.

   THE PHONE IS THE PRIMARY DEVICE. Somebody is filling this in
   at eleven at night, in bed, on a small screen. So: one column
   everywhere, nothing that scrolls sideways, boxes at least as
   tall as a fingertip, and 16px type in every field - anything
   smaller makes iOS zoom the page on focus, which is how you
   lose your place in a long form.
   ============================================================ */

.plan-form { display: block; max-width: none; }

.plan-section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px 16px; flex-wrap: wrap;
}
/* The section heading is a heading here rather than the app's small muted
   label: on this page it is the thing that tells you where you are in a very
   long form, and it has to be findable while scrolling past at speed. */
.plan-section > .plan-section-head h2 {
  margin: 0 0 4px;
  font-size: 17px; text-transform: none; letter-spacing: 0;
  color: var(--ink); font-weight: 700;
}
/* Answered out of asked. A count, deliberately, and never a warning: a blank
   is a perfectly good answer to any of these, so nothing here goes red or
   nags, and it is drawn in the quietest ink on the page. */
.plan-progress { margin: 0; font-size: 12px; color: var(--muted); white-space: nowrap; }

.plan-intro {
  margin: 0 0 14px; padding-left: 12px;
  border-left: 3px solid var(--accent);
  color: var(--muted);
}

/* "I don't ask this any more, but what you said is still here." */
.plan-retired-note {
  margin: 0 0 14px; padding: 8px 12px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg); color: var(--muted); font-size: 13px;
}

.plan-question { padding: 14px 0; border-top: 1px solid var(--line); }
.plan-question:first-of-type { border-top: none; }
.plan-question.is-retired { opacity: .92; }

.plan-field { display: flex; flex-direction: column; gap: 6px; }
.plan-label { font-size: 15px; font-weight: 600; color: var(--ink); }
.plan-helper {
  display: block; font-size: 13px; font-weight: 400; color: var(--muted);
  margin: 0 0 2px;
}

/* A retired question is still perfectly readable - somebody has to be able to
   change their answer to it - so this is a quiet label rather than a strike
   through or a grey-out. */
.plan-retired-flag {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--bg); color: var(--muted);
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
  vertical-align: 1px;
}

.plan-question input[type="text"],
.plan-question textarea {
  width: 100%; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--input); color: var(--ink);
  /* 16px, not 15px: anything smaller makes iOS zoom the page when the box is
     focused, and a page that jumps every time you tap a field is a page
     nobody finishes. */
  font: 16px/1.5 inherit; font-family: inherit;
  min-height: var(--control-h);
}
.plan-question textarea { resize: vertical; }

/* ---------- Pick one ---------- */
.plan-choice { border: none; margin: 0; padding: 0; min-width: 0; }
.plan-choice > legend { padding: 0; }

.plan-option {
  display: flex; align-items: center; gap: 10px;
  /* Tall enough to hit with a thumb without aiming. */
  min-height: 40px; padding: 4px 0;
  cursor: pointer;
}
.plan-option input[type="radio"] { width: 18px; height: 18px; margin: 0; flex: 0 0 auto; }
.plan-option > span { font-size: 15px; }
/* The way back out of a radio group. Quieter than the real choices, because
   it is not one of them - it is how you take an answer back. */
.plan-option-blank > span { color: var(--muted); font-size: 13px; }

.plan-other { margin-top: 4px; }

/* ---------- Save, and the line that says it saved ---------- */
.plan-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-bottom: 20px;
}
/* Written by public/app.js, and empty unless a save has actually FAILED -
   success is said beside the answer instead, by .plan-saved below. */
.plan-status { font-size: 13px; color: var(--muted); }
.plan-status.is-problem { color: var(--danger); }

/* ---------- The quiet "Saved", beside the answer ---------- */

/* THE THING YOU NOTICE ONCE AND THEN STOP NOTICING.

   Absolutely positioned, so it costs no room and takes none: it can appear
   and go again without a single pixel of the page moving. That is the whole
   requirement - anything that reflows the paragraph somebody is reading is
   worse than saying nothing.

   It fades rather than blinking out, and it never animates in: arriving
   quietly and leaving quietly is what makes it possible to ignore. */
.plan-question { position: relative; }
.plan-saved {
  position: absolute; top: 2px; right: 0;
  font-size: 12px; color: var(--muted);
  opacity: 0; transition: opacity .25s ease;
  pointer-events: none;
}
.plan-saved.is-shown { opacity: 1; }
.plan-saved.is-shown::before { content: "✓ "; }

/* Somebody who has asked for less movement gets none: it is simply there or
   not there. */
@media (prefers-reduced-motion: reduce) {
  .plan-saved { transition: none; }
}

/* What the page says after a plain form post - the no-JavaScript path. */
.plan-said {
  margin: 0 0 20px; padding: 10px 14px;
  border: 1px solid var(--notice-line); border-radius: var(--radius);
  background: var(--notice-bg); color: var(--notice-ink);
}

.plan-preamble p { margin: 0 0 10px; }
.plan-preamble p:last-child { margin-bottom: 0; }
.plan-count { color: var(--muted); }
.plan-sent {
  padding: 10px 14px;
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); background: var(--bg);
}

.plan-send p { margin: 0 0 12px; }
.plan-flag { margin-top: 14px; }
.plan-flag > summary {
  cursor: pointer; font-size: 13px; color: var(--muted); padding: 4px 0;
}
.plan-flag > summary:hover { color: var(--ink); }
.plan-flag form { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
.plan-flag textarea {
  width: 100%; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--input); color: var(--ink);
  font: 16px/1.5 inherit; font-family: inherit;
}
.plan-flag .btn { align-self: flex-start; }

/* The way in to the form, on each event in the client's list. */
.event-plan { flex: 0 0 auto; display: flex; flex-direction: column; gap: 4px; }

/* ON A PHONE IT TAKES THE WHOLE WIDTH, which is the one place in this
   stylesheet that asks how wide the window is. Everything else here wraps on
   its own and looks right at any size; this does too, and looking right is
   not the standard for the single thing a client came to press. A button
   sitting at its natural width under a wrapped event reads as a footnote to
   the line above it. The rest of the page is unchanged - this rule only ever
   touches the one button. */
@media (max-width: 560px) {
  .event-plan { flex: 1 1 100%; }
  .event-plan .btn-plan { width: 100%; text-align: center; justify-content: center; }
}

/* ---------- What has changed ---------- */
.plan-history { list-style: none; margin: 0; padding: 0; }
.plan-history li {
  display: flex; flex-wrap: wrap; gap: 2px 16px;
  padding: 10px 0; border-top: 1px solid var(--line);
}
.plan-history li:first-child { border-top: none; padding-top: 2px; }
.plan-history-when { flex: 0 1 170px; color: var(--muted); font-size: 13px; }
.plan-history-what { flex: 1 1 240px; }
.plan-history-who { flex: 1 1 100%; color: var(--muted); font-size: 12px; }
.plan-history-fold > summary { cursor: pointer; color: var(--muted); font-size: 13px; }
.plan-history-fold > summary:hover { color: var(--ink); }

/* The planning form's own heading on Joel's gig page, which is a second h1 on
   a page that already has one. Sized down so it reads as a divider between
   two halves of the page rather than as a second title. */
.plan-heading { font-size: 19px; }

/* ---------- ANSWERED, OR STILL BLANK - ON JOEL'S COPY ONLY ----------
   The question his page has to answer at a glance is "what do I still not
   know about this wedding", and forty-eight questions scrolling past all
   drawn the same way do not answer it. So on his copy - and only his - an
   answered question carries a rule down its left edge and a blank one drops
   its label to the muted ink. The client's own form never does this: telling
   somebody which of their answers are missing, every time they open the
   page, is nagging, and a blank is a perfectly good answer.

   It follows what is on the page rather than what was on it when it loaded:
   public/app.js rewrites data-answered as he types, so the mark appears the
   moment an answer does. */
.plan-form[data-plan-mode="owner"] [data-answered="yes"] {
  border-left: 3px solid var(--accent);
  padding-left: 12px;
}
.plan-form[data-plan-mode="owner"] [data-answered="no"] .plan-label { color: var(--muted); }

/* ============================================================
   PICKING A TIME, AND PICKING A DATE
   The start and end boxes on the gig form, and the date and
   time answers on the planning form. Underneath each one is a
   plain text box that still takes a typed value; everything
   here is the panel public/app.js hangs beside it, and none of
   it exists until app.js builds it.
   ============================================================ */

.timebox, .datebox { position: relative; display: flex; align-items: stretch; }
.timebox > input, .datebox > input { flex: 1 1 auto; min-width: 0; }

/* THERE IS NO LITTLE ARROW ON THE END ANY MORE, and its absence is the point
   rather than a tidy-up. A chevron at the right-hand edge that was the only
   thing which opened the list taught people that the box had one clickable
   spot in it - so most of them never opened the list at all. The whole field
   opens it now, and an affordance for something the whole field already does
   is a second thing to explain. The padding that used to keep text clear of
   the chevron went with it. */

/* NINETY-SIX TIMES, AND ALL OF THEM REACHABLE. A fixed height with its own
   scrollbar is the whole of what the old datalist could not do. Tall enough
   to show a couple of hours at a time so scrolling has context, short enough
   to leave the form visible underneath it. */
.timebox-list, .datebox-panel {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 40;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 8px; box-shadow: 0 10px 30px var(--shadow);
}
.timebox-list {
  right: 0;
  max-height: 260px; overflow-y: auto; overscroll-behavior: contain;
  margin: 0; padding: 4px; list-style: none;
}
.timebox-list[hidden], .datebox-panel[hidden] { display: none; }
.timebox-list > li {
  padding: 7px 10px; border-radius: 6px; cursor: pointer;
  /* A thumb's worth of target on a phone, where this list is a lot of small
     rows close together. */
  min-height: 34px; display: flex; align-items: center;
}
/* A row the typing has narrowed away. The rule above sets display, so this has
   to say so explicitly - the browser's own [hidden] rule is weaker than a
   class selector and would quietly lose. */
.timebox-list > li[hidden] { display: none; }
.timebox-list > li:hover { background: var(--focus-field); }
/* Where the keyboard is. It has to be legible against the panel in both
   themes without being the accent colour, which on this list would read as
   "chosen" rather than "here". */
.timebox-list > li.is-on {
  background: var(--accent); color: var(--accent-ink); font-weight: 600;
}

/* ---------- A MONTH AT A TIME ---------- */

/* The calendar hanging under a date box. It is deliberately the same shape as
   the time list - same offset, same panel, same shadow - because they are the
   same idea and appear on the same form, and two panels that looked different
   would read as two different kinds of thing. */
.datebox-panel { padding: 10px; width: max-content; }

.datebox-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 8px;
}
.datebox-month { font-size: 14px; font-weight: 600; }
.datebox-step {
  width: 30px; height: 30px; padding: 0; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--input); color: var(--ink); font: inherit; cursor: pointer;
}
.datebox-step:hover { border-color: var(--focus-line); }

.datebox-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
}
.datebox-dayname {
  font-size: 11px; color: var(--muted); text-align: center; padding: 2px 0;
}
.datebox-day {
  /* A thumb's worth of target, for the same reason the time rows are. */
  min-width: 34px; min-height: 34px; padding: 0;
  border: 1px solid transparent; border-radius: 8px;
  background: none; color: var(--ink); font: inherit; font-size: 14px;
  cursor: pointer;
}
.datebox-day:hover { background: var(--focus-field); }
.datebox-day.is-empty { visibility: hidden; cursor: default; }
.datebox-day.is-today { border-color: var(--line); font-weight: 600; }
.datebox-day.is-on {
  background: var(--accent); color: var(--accent-ink); font-weight: 600;
}

/* ============================================================
   THE QUESTION EDITOR
   Joel's, and only ever Joel's. Same rule as everything above:
   not one colour here that is not already a variable.
   ============================================================ */

.qe-rule {
  margin: 0 0 20px; padding: 10px 14px; max-width: 720px;
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); background: var(--panel);
}

.qe-section.is-retired { border-style: dashed; }

.qe-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 10px 20px; flex-wrap: wrap;
  padding-bottom: 12px; border-bottom: 1px solid var(--line);
}
.qe-what { flex: 1 1 260px; min-width: 0; }
.qe-title {
  margin: 0 0 4px; font-size: 17px; font-weight: 700;
  text-transform: none; letter-spacing: 0; color: var(--ink);
}
.qe-intro { margin-top: 4px; }
.qe-helper { margin-top: 2px; }

/* Every button that acts on one section or one question, in one row that
   wraps onto its own line on a phone rather than squeezing the words beside
   it into a column two characters wide. */
/* flex: 0 1 auto, not 0 0 auto - the middle number is "may shrink", and
   without it this row took its full content width whatever the room, so its
   own flex-wrap never got a chance to do anything and a phone got nineteen
   pixels of sideways scroll. It still never shrinks when there IS room. */
.qe-actions {
  flex: 0 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.qe-actions form { margin: 0; }
/* The two groups wrap INSIDE themselves as well as beside each other. Six
   controls on one row - two arrows, Edit, Retire and sometimes Delete - is more
   than a phone has room for, and the pair that could not break was pushing the
   whole page nineteen pixels sideways. */
.qe-move, .qe-state { display: flex; gap: 6px; flex-wrap: wrap; }
/* Square, so the two arrows read as one control rather than as two buttons
   that happen to be next to each other. */
.qe-arrow { width: var(--control-h); padding: 0; font-size: 15px; }
.qe-arrow[disabled] { opacity: .4; cursor: default; }
.qe-arrow[disabled]:hover { border-color: var(--line); }

.qe-list { list-style: none; margin: 0; padding: 0; }
.qe-question {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 10px 20px; flex-wrap: wrap;
  padding: 12px 0; border-bottom: 1px solid var(--line);
}
.qe-question.is-retired .qe-what > strong { color: var(--muted); }
.qe-flag {
  display: inline-block; margin-left: 6px; padding: 1px 7px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--bg); color: var(--muted);
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
  vertical-align: 1px;
}
/* ---------- ADDING ONE, AT THE BOTTOM OF WHAT IT ADDS TO ----------

   Both of these used to be buttons ABOVE the thing they add to - "+ Section"
   in the top corner, "+ Question in this section" over that section's
   questions - and both were wrong in the same way. A new one appears at the
   END of the list, so a control at the top sends you looking in the wrong
   place, and a filled button competing with sixty questions is the loudest
   thing on a page about reading them.

   So: below, small, and quiet. A plus and one word, in the muted ink, which
   lifts to the accent when the mouse is on it. It is still a whole line and a
   comfortable tap target - quiet is not the same as fiddly. */
/* "+ Question" sits INSIDE a section card, under the questions it adds to.
   "+ Section" sits under the whole list and adds to that.

   THEY LOOKED THE SAME AND SAT FOUR PIXELS APART, which is how "Add section"
   went missing: after the last card there were two identical quiet links
   stacked on top of each other, and the outer one read as one more control
   belonging to the card above it. It has room of its own now, and a rule
   above it that says the list has ended - so it is plainly the thing you do
   after all the sections rather than something you do to the last one. Still
   small and still secondary: adding a section is rare, and reading the
   questions is what this page is for. */
.qe-add { margin: 14px 0 0; }
.qe-add-section {
  margin: 26px 0 32px; padding-top: 20px;
  border-top: 1px solid var(--line);
}

.qe-add-link {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 34px; padding: 4px 2px;
  color: var(--muted); font-size: 14px; text-decoration: none;
}
.qe-add-link:hover { color: var(--accent); text-decoration: none; }
.qe-add-plus {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border: 1px solid var(--line); border-radius: 6px;
  font-size: 14px; line-height: 1;
}
.qe-add-link:hover .qe-add-plus { border-color: var(--accent); }

/* ---------- DRAGGING A ROW SOMEWHERE ELSE ----------

   An ADDITION to the arrow buttons and never a replacement - the arrows are
   the keyboard's way, the no-JavaScript way and the phone's way. Everything
   below is built by public/app.js, so with JavaScript off there is no handle
   and nothing that looks like one.

   The two things it has to make obvious are what is being dragged and where it
   will land, and there is one rule for each: the row that left goes
   translucent, and a line is drawn in the gap it would drop into. */

.drag-grip {
  flex: 0 0 auto; align-self: flex-start;
  width: 22px; min-height: 26px; margin-right: 2px;
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 15px; line-height: 1;
  cursor: grab; user-select: none;
}
/* The lift, and the settle back out of it. Not applied to the gap, which
   appears and goes rather than travelling. */
[data-drag-item] { transition: transform 160ms ease, box-shadow 160ms ease, opacity 160ms ease; }
@media (prefers-reduced-motion: reduce) {
  [data-drag-item] { transition: none; }
  [data-drag-item].is-dragging { transform: none; }
}
.drag-grip:active { cursor: grabbing; }
.drag-grip:hover { color: var(--ink); }

/* PICKED UP. Still in the list, so nothing reflows under the pointer - and
   clearly lifted OFF it: a degree of tilt, a little smaller, a shadow under
   it, and translucent. "Have I got hold of it?" answered without looking
   anywhere except at the row. */
[data-drag-item].is-dragging {
  opacity: .55;
  transform: scale(.985) rotate(-.4deg);
  box-shadow: 0 14px 34px var(--shadow);
  cursor: grabbing;
}

/* And the list quietens around it, so the thing being carried is the only
   thing at full strength. */
[data-drag-list].is-rearranging > [data-drag-item]:not(.is-dragging) { opacity: .72; }

/* WHERE IT WILL LAND, AS THE SHAPE OF THE HOLE IT WILL FILL.

   IT USED TO BE A THREE PIXEL RULE and that was the problem: a line between
   two rows says where a boundary is and nothing at all about what is going
   into it, so on a list of sixty questions it read as an underline on the row
   above. This is a GAP the height of the row being carried, opening where the
   row would go - the question answered rather than a mark to interpret. The
   dashed edge and the accent say it is a hole rather than a row. */
.drag-gap {
  border: 2px dashed var(--accent); border-radius: var(--radius);
  background: var(--settle);
  margin: 6px 0; list-style: none; opacity: .8;
}

/* WHERE THE GRIP SITS.

   A question row is already a flex row, so its grip is simply the first thing
   in it and the layout absorbs it.

   A section is a card, so its grip goes in the card's own left padding, and
   the card is given a little more of it to make room. Both rules are written
   with :has(), so they apply ONLY when app.js has actually put a grip there -
   with JavaScript off there is no handle and no gap where one would have
   been. A browser without :has() gets the handle sitting slightly over the
   heading, which is untidy rather than broken. */
.qe-section { position: relative; }
.qe-section:has(> .drag-grip) { padding-left: 38px; }
.qe-section > .drag-grip { position: absolute; top: 18px; left: 10px; }

/* ---------- The question editor's own form ---------- */

/* The question itself is the thing this page is about, so it is the biggest
   thing on it. Everything else is a setting. */
.qe-form { max-width: 660px; }
.qe-question-label > input { font-size: 17px; padding: 10px 12px; }

/* Enter in a text box saves, rather than pressing whichever submit button
   happens to come first. See the note in views/questions/question.ejs. */
.qe-enter-saves {
  position: absolute; width: 1px; height: 1px; padding: 0; border: 0;
  opacity: 0; pointer-events: none;
}

/* Answer type, and the options that belong to some of the types. */
.qe-answer {
  display: flex; flex-direction: column; gap: 14px;
  border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px;
}

/* The label beside the dropdown rather than stacked above it: two words and a
   control, on one line, reads in a glance. It wraps on a narrow screen, where
   stacked is the only thing that fits. */
.qe-type { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.qe-type > label { font-size: 13px; font-weight: 600; flex: 0 0 auto; }
.qe-type > select { width: auto; min-width: 220px; flex: 1 1 220px; }

/* THE OPTIONS SIMPLY ARE NOT THERE FOR A TYPE THAT HAS NONE.

   Not disabled, not greyed out with a note saying when they apply - gone. The
   test is the SELECTED OPTION inside the dropdown, which :has() re-evaluates
   as the dropdown changes, so this follows the control live with no
   JavaScript at all. Same trick as .referral above.

   A browser without :has() shows them all the time, which is untidy rather
   than broken - and the server ignores options on a type that has none, so
   nothing can be stored by the difference. */
.qe-answer:not(:has(
  option[value="choice"]:checked,
  option[value="checkboxes"]:checked,
  option[value="dropdown"]:checked,
  option[value="choice_other"]:checked
)) .qe-choices { display: none; }

.qe-choices { display: flex; flex-direction: column; gap: 8px; }
.qe-choices-head { font-size: 13px; font-weight: 600; }

.qe-choice-list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: 6px; counter-reset: qe-choice; }

/* One row per choice: what it will look like, the words, and the three
   things you can do to it. */
.qe-choice { display: flex; align-items: center; gap: 6px; }
.qe-choice > input { flex: 1 1 auto; min-width: 0; }

/* The mark in front of the box is drawn as the control the choice will
   actually become, so the row looks like the thing it makes: a ring for a
   pick-one, a square for a tick, a number for a dropdown. Drawn in CSS from
   the type currently selected, so it follows the dropdown live. */
.qe-choice-mark {
  flex: 0 0 auto; width: 16px; height: 16px; border: 2px solid var(--muted);
  border-radius: 50%; counter-increment: qe-choice;
}
.qe-answer:has(option[value="checkboxes"]:checked) .qe-choice-mark { border-radius: 4px; }
.qe-answer:has(option[value="dropdown"]:checked) .qe-choice-mark {
  border: none; width: auto; min-width: 16px; color: var(--muted);
  font-size: 13px; text-align: right;
}
.qe-answer:has(option[value="dropdown"]:checked) .qe-choice-mark::before {
  content: counter(qe-choice) ".";
}

.qe-choice-drop { color: var(--muted); font-size: 18px; line-height: 1; }
.qe-choice-drop:hover { color: var(--danger); border-color: var(--danger-line); }
.qe-choice-add { align-self: flex-start; }

/* ---------- Which events ---------- */

.qe-applies { border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin: 0; }
.qe-applies > legend { padding: 0 6px; font-size: 13px; font-weight: 600; }
/* Each radio and its words are one thing, left aligned, on one line - which
   is what the width fix on .form input[type="radio"] above buys. The rows are
   given a little height so they are a thumb's worth of target on a phone. */
.qe-applies .check-row { min-height: 32px; }
.qe-applies-list {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 4px 14px; margin-top: 6px; padding-left: 26px;
}
/* Nothing is being decided by this list while "every kind of event" is
   chosen, so it says so quietly. It stays entirely usable: ticking one is
   how somebody chooses "only these", and app.js moves the radio for them. */
.qe-applies:has(input[value="all"]:checked) .qe-applies-list { opacity: .55; }

/* The glyph in front of an answer type in the question list, matching the
   one in the editor's dropdown. */
.qe-type-mark { display: inline-block; min-width: 1em; margin-right: 2px; }

/* Where a row lands when the page has just reloaded onto it - see the note
   on the id in views/questions/index.ejs. Enough room above it that it is
   clearly inside the page rather than jammed against the top of the window. */
[data-qe-row] { scroll-margin-top: 90px; }
/* It is focused so the KEYBOARD lands there too, and it is not a control, so
   it gets no ring of its own. What marks it is that it is the thing you are
   looking at. */
[data-qe-row]:focus { outline: none; }

/* ============================================================
   ON PAPER
   The printable planning sheet at /gigs/N/plan/print, which Joel
   works the night off. See views/gigs/plan-print.ejs.

   There is no library and nothing is generated: it is an
   ordinary page, and the browser's own Print turns it into
   paper. This is the whole of what makes it fit to print.
   ============================================================ */

.plan-print { max-width: 720px; margin: 0 auto; padding: 24px; }
.plan-print-head { margin-bottom: 24px; }
.plan-print-head h1 { margin-bottom: 6px; }
.plan-print-head p { margin: 2px 0; color: var(--muted); }
.plan-print-state { font-size: 13px; }
.plan-print-section { margin-bottom: 22px; }
.plan-print-section h2 {
  margin: 0 0 8px; padding-bottom: 4px;
  font-size: 13px; color: var(--ink);
  border-bottom: 1px solid var(--line);
}
.plan-print-section dl { margin: 0; }
.plan-print-section dt { font-weight: 600; margin-top: 10px; }
.plan-print-section dd {
  margin: 2px 0 0;
  /* Pasted song lists arrive with their line breaks, and they mean something -
     one song per line is how they were written and how they get read out. */
  white-space: pre-wrap;
}
.plan-print-empty { color: var(--muted); }

@media print {
  /* Ink is expensive and a dark page on paper is unreadable. Whatever theme
     the screen is in, paper is black on white. */
  .plan-print {
    background: #fff; color: #000;
    max-width: none; margin: 0; padding: 0;
  }
  /* Nothing you can press is any use once it is printed, and neither is a
     confirmation about something that happened before the page was opened. */
  .plan-print-back, .flash-mark { display: none; }
  .plan-print-head { border-bottom: 1px solid #999; padding-bottom: 8px; }
  .plan-print-head p, .plan-print-section h2, .plan-print-empty { color: #000; }
  .plan-print-section h2 { border-bottom: 1px solid #999; }
  /* A question and its answer belong on the same page as each other, and a
     section heading at the foot of a page with nothing under it is a page
     turned for no reason. */
  .plan-print-section { break-inside: avoid; page-break-inside: avoid; }
  .plan-print-section dt { break-after: avoid; page-break-after: avoid; }
  a { color: #000; text-decoration: none; }
}

/* ============================================================
   A LITTLE FORMATTING, ANYWHERE SOMEBODY TYPES MORE THAN A LINE
   Bold, italic, underline, bullets, numbers, a link. Built by
   public/app.js on top of a plain textarea, so with JavaScript
   off none of this exists and the textarea is the control.

   THE NAMES SAY `rich` RATHER THAN `plan` BECAUSE IT IS NOT
   THE PLANNING FORM'S ANY MORE. The same control upgrades the
   notes boxes on the client page and the gig page. One
   control, one set of rules, one place to fix it.
   ============================================================ */

.rich-bar {
  display: flex; gap: 4px; flex-wrap: wrap;
  margin: 6px 0 0;
}
.rich-tool {
  min-width: 34px; min-height: 34px; padding: 0 8px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--input); color: var(--ink);
  font: inherit; font-size: 14px; line-height: 1; cursor: pointer;
}
.rich-tool:hover { border-color: var(--focus-line); }

/* The editable area itself, drawn to look exactly like the textarea it stands
   in for - same border, same background, same 16px type that stops iOS
   zooming the page when a field is tapped. Somebody typing into this should
   not be able to tell it is not an ordinary box. */
.rich-box {
  width: 100%; margin-top: 6px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--input); color: var(--ink);
  font: 16px/1.5 inherit; font-family: inherit;
  /* Roughly the four rows the textarea starts at, and it grows on its own
     from there - which is what a pasted list of forty songs needs. */
  min-height: 96px;
  overflow-wrap: break-word;
}
.rich-box:focus {
  outline: none;
  border-color: var(--focus-line); background: var(--focus-field);
}
/* The notes box is a shorter thing than a planning answer - it sits under a
   heading on a page of other things rather than being the whole of what the
   page is for. */
.rich-field .rich-box { min-height: 72px; }
.rich-field { width: 100%; }

/* What the few allowed tags look like, wherever they are drawn: in the box
   being typed into, in a note already written, in a printed answer, and on
   Joel's copy of the form. One list, so a note and an answer never look like
   two different things. */
.rich-box p, .note-body p, .plan-print-answer p { margin: 0 0 8px; }
.rich-box p:last-child, .note-body p:last-child,
.plan-print-answer p:last-child { margin-bottom: 0; }
.rich-box ul, .rich-box ol, .note-body ul, .note-body ol,
.plan-print-answer ul, .plan-print-answer ol { margin: 0 0 8px; padding-left: 22px; }
.rich-box a, .note-body a, .plan-print-answer a { color: var(--accent); }

/* WHAT TAB MAKES when the caret is not in a list. The browser's editing mode
   produces a blockquote for "indent" and hangs a style on it; the style is
   dropped on the way into the database along with every other attribute, so
   the indent is THIS rule and not that one - which is why it looks the same
   in the box, on Joel's copy and on the printed page. No quotation marks and
   no bar down the side: it is an indent, not a quotation. */
.rich-box blockquote, .note-body blockquote, .plan-print-answer blockquote {
  margin: 0 0 8px 26px; padding: 0; border: 0;
}
.rich-box blockquote:last-child, .note-body blockquote:last-child { margin-bottom: 0; }
.note-body { margin: 0; }

/* ============================================================
   "SHE SAYS THIS ISN'T RIGHT."
   On Joel's gig page, at the top, in the alarm colours until he
   marks it handled. See the note in views/gigs/show.ejs.
   ============================================================ */

.gig-flags.has-open {
  border-color: var(--error-line);
  border-left: 4px solid var(--danger);
  background: var(--error-bg);
}
.gig-flags.has-open h2 { color: var(--error-ink); }

.flag-list { list-style: none; margin: 0; padding: 0; }
.flag {
  padding: 12px 0; border-top: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
}
.flag:first-child { border-top: none; padding-top: 4px; }
.flag-said { margin: 0; white-space: pre-wrap; }
.flag-meta { margin: 0; font-size: 13px; color: var(--muted); }
.gig-flags.has-open .flag:not(.is-handled) .flag-meta { color: var(--error-ink); }
/* A flag he has dealt with is still here - what she said and the fact he saw
   it are both worth keeping - and it is visibly done rather than gone. */
.flag.is-handled { opacity: .7; }
.flag-handled { font-weight: 600; }

/* The client's own copy, beside the details she is disputing, so she can see
   that it went somewhere and does not send it again. */
.plan-flag-list { list-style: none; margin: 14px 0 0; padding: 0; }
.plan-flag-said {
  padding: 10px 12px; margin-bottom: 8px;
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); background: var(--bg);
}
.plan-flag-said p { margin: 0; }
.plan-flag-said.is-handled { border-left-color: var(--notice-line); }

/* ============================================================
   WHEN A CLIENT HAS ACTUALLY BEEN IN
   ============================================================ */

.access-heading {
  margin: 18px 0 6px; font-size: 13px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
}
.signin-list {
  list-style: none; margin: 0; padding: 0;
  font-variant-numeric: tabular-nums;
}
.signin-list li {
  padding: 5px 0; border-top: 1px solid var(--line); font-size: 14px;
}
.signin-list li:first-child { border-top: none; }
/* The most recent one is the one he is looking for. */
.signin-list li:first-child { font-weight: 600; }
