/*
 * Flag component styles. Tokens live in tokens.css.
 *
 * Structure carries information here: a rule means a boundary, a wash means a
 * different kind of surface, weight means a label rather than data. Nothing is
 * rounded, nothing is shadowed except a modal, and there are no gradients.
 */

/* ---------------------------------------------------------------- Reset -- */

*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking-body);
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; height: auto; display: block; }
hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-8) 0; }

/* ------------------------------------------------------------ Typography -- */

h1, h2, h3, h4 { margin: 0; font-weight: 400; line-height: var(--leading-tight); }
h1 { font-size: var(--text-display); letter-spacing: var(--tracking-display); }
h2 { font-size: var(--text-title); letter-spacing: var(--tracking-title); }
h3 { font-size: var(--text-section); font-weight: 500; }
p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

.lede { color: var(--ink-muted); margin-bottom: var(--space-6); }
.muted { color: var(--ink-muted); }
.small { font-size: var(--text-small); }

.label {
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 500;
}

/* Money and any column of figures. Without this, prices in a table jitter
   column-to-column and become hard to scan or check. */
.numeric, .money, td.numeric, th.numeric { font-variant-numeric: tabular-nums; }

/* The admin app uses <code> for things a person reads off the screen and
   types somewhere else: a Xero redirect URI, a SKU, a payment link. All of
   them are long and none of them have spaces to break at, so without this one
   line they push a panel wider than the page on a tablet. Added 21 September
   with the till's payment link, which is the narrowest column any of them
   appear in. */
code { overflow-wrap: anywhere; }
.money { text-align: right; }
.mono { font-family: var(--font-mono); font-size: var(--text-small); white-space: nowrap; }

a { color: var(--ink); text-decoration: underline; text-underline-offset: 0.2em; text-decoration-thickness: 1px; }
a:hover { text-decoration-color: var(--sage); }

/* ----------------------------------------------------------------- Focus -- */
/* The live site has no visible focus ring at all. Every interactive element
   here shows one, and it is the same ring everywhere so it is learnable. */

:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.skip-link {
  position: absolute; left: -9999px;
  background: var(--ink); color: var(--paper);
  padding: var(--space-2) var(--space-4); z-index: var(--z-toast);
}
.skip-link:focus { left: var(--space-4); top: var(--space-2); }

/* ----------------------------------------------------------- App shell --- */

.app-header {
  border-bottom: 1px solid var(--line);
  background: var(--paper);
  position: sticky; top: 0; z-index: var(--z-header);
}
.app-header__bar {
  max-width: var(--container); margin: 0 auto;
  padding: var(--space-3) var(--space-6);
  display: flex; align-items: center; gap: var(--space-4);
}

/* Wordmark, set the way the gallery sets it: name over a spaced descriptor. */
.wordmark { text-decoration: none; line-height: 1.05; flex: none; }
.wordmark__name {
  display: block; font-size: 1.0625rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.wordmark__sub {
  display: block; font-size: 0.5625rem; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--ink-muted);
}

.app-nav { display: flex; gap: var(--space-4); flex: 1; flex-wrap: wrap; min-width: 0; } /* 16px between the eight items (it was 20) and 6px between the person's links (it was 8), and the bar's two rows start below 90rem (it was 88): the person's picture beside their name is 32px the bar had to find room for */
.app-nav a {
  text-decoration: none; white-space: nowrap;
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  padding: var(--space-2) 0;
  border-bottom: 2px solid transparent;
}
.app-nav a:hover { border-bottom-color: var(--line); }
.app-nav a[aria-current='page'] { border-bottom-color: var(--ink); }

/* Sign out is a form (a POST), drawn as the link it replaced. */
.signout { margin: 0; display: inline; }
.signout button {
  all: unset; cursor: pointer; color: var(--ink); text-decoration: underline;
  text-underline-offset: 0.2em; text-decoration-thickness: 1px;
}
.signout button:hover { text-decoration-color: var(--sage); }
.signout button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.app-header__who {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--text-small); color: var(--ink-muted); flex: none;
}
.env-badge {
  font-size: var(--text-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; padding: 0.15rem var(--space-2);
  background: var(--warn-wash); color: var(--warn); border: 1px solid var(--warn);
}

.app-main { max-width: var(--container); margin: 0 auto; padding: var(--space-8) var(--space-6) var(--space-16); }
.app-footer {
  max-width: var(--container); margin: 0 auto;
  padding: 0 var(--space-6) var(--space-8);
  color: var(--ink-muted); font-size: var(--text-small);
}

/* Where the eight items and the person's own links stop fitting on one line (a laptop, a tablet), the bar says what the layout is: the wordmark and the person's
   links on the first row, the eight items on a row of their own. Left to wrap, "Settings" was stranded on a second line beside the wordmark at 1440 pixels. */
@media (max-width: 89.99rem) {
  .app-header__bar { flex-wrap: wrap; gap: var(--space-2) var(--space-3); }
  .app-header__who { margin-left: auto; }
  .app-nav { order: 3; flex: 1 1 100%; width: 100%; }
}
@media (max-width: 60rem) {
  .app-main { padding: var(--space-6) var(--space-4) var(--space-12); }
  .app-footer { padding-inline: var(--space-4); } /* the footer's words stand where the page's do */
}

/* --------------------------------------------------------- The Menu ----- */

/*
 * A phone, or a window narrower than 64rem (30 October 2026, David: "a hamburger for the menu rather than lots of items jammed on the top of the screen"). The bar was
 * three rows and 300 pixels of a 375 pixel phone (the wordmark, the person's links, the eight items), more than a third of the screen above the first line of the page.
 * Now it is one short row: the wordmark, the count of open tasks and the environment where there are any, and a Menu button. The button is a <details> (no script is needed to open it),
 * and its panel lays the eight items out as rows a thumb can press, with the page you are on marked, and then the person's own links (public/app-menu.js adds Escape, a click
 * elsewhere and aria-expanded). The panel is drawn over the page, under the bar, and scrolls if it is longer than the screen; the page behind is dimmed and pressing it closes the menu.
 * From 64rem the bar draws the row of links itself, as it always did, and none of this is drawn.
 */
.app-menu { display: none; }
.app-menu__button { list-style: none; }
.app-menu__button::-webkit-details-marker { display: none; }
.app-menu__button::marker { content: ''; }
.app-menu__icon--close { display: none; }
.app-menu[open] .app-menu__icon--close { display: block; }
.app-menu[open] .app-menu__icon--bars { display: none; }

@media (max-width: 63.99rem) {
  html { scroll-padding-top: 4.5rem; } /* a link to a place on the page (a skipped heading, a #paused) is not left under the bar */
  /* The bar is a single row; its background is its own (not the header's) so the dimming of the page behind the open menu can pass under it. */
  .app-header { background: transparent; }
  .app-header__bar {
    flex-wrap: nowrap; gap: var(--space-2); background: var(--paper);
    padding: var(--space-2) max(var(--space-4), env(safe-area-inset-right)) var(--space-2) max(var(--space-4), env(safe-area-inset-left));
  }
  .app-nav { display: none; }
  .app-header .wordmark { display: inline-flex; flex-direction: column; justify-content: center; min-height: var(--control-height); padding-block: 0; } /* a box 44 pixels high to press, as the Menu button beside it is */
  /* Of the person's group the count and the environment stay; the rest is in the Menu. */
  .app-header__who { margin-left: auto; flex: 0 1 auto; min-width: 0; gap: var(--space-2); }
  .app-header .app-header__who > :not(.task-badge, .env-badge) { display: none; }
  .app-header .env-badge { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* on the narrowest phone with a long name and a count, the environment gives way before the Menu button does */
  .app-header .task-badge { flex: none; }
  .app-header .app-header__who .task-badge { min-width: var(--control-height); height: var(--control-height); }
  .app-header .app-header__who .task-badge::after { content: none; }
  .app-menu { display: block; flex: none; } /* not positioned: the panel is placed against the header */
  .app-menu > .app-menu__button {
    display: flex; align-items: center; gap: var(--space-2);
    min-height: var(--control-height); padding: 0 var(--space-4);
    border: 1px solid var(--ink); background: var(--paper); color: var(--ink);
    font-size: var(--text-small); letter-spacing: var(--tracking-label); text-transform: uppercase;
    cursor: pointer; user-select: none;
  }
  .app-menu > .app-menu__button:hover { background: var(--wash); }
  .app-menu[open] > .app-menu__button { background: var(--ink); color: var(--paper); }
  .app-menu__icon { flex: none; width: 1.25rem; height: 1.25rem; }
  /* The page behind, dimmed. It belongs to the button, so pressing it closes the menu with no script. It starts under the bar's own background. */
  .app-menu[open] > .app-menu__button::before {
    content: ''; position: fixed; inset: 0; z-index: -1; background: rgb(20 20 20 / 40%);
  }
  .app-menu__panel {
    position: absolute; top: 100%; left: 0; right: 0;
    max-height: calc(100vh - 4.5rem); max-height: calc(100dvh - 4.5rem);
    overflow-y: auto; overscroll-behavior: contain;
    background: var(--paper); border-bottom: 1px solid var(--ink);
    padding: var(--space-2) max(var(--space-4), env(safe-area-inset-right)) var(--space-4) max(var(--space-4), env(safe-area-inset-left));
  }
  .app-menu__nav { max-width: 40rem; }
  .app-menu__list { list-style: none; margin: 0; padding: 0; }
  .app-menu__link, .app-menu__row, .app-menu__account .person, .app-menu__account .signout button {
    display: flex; align-items: center; gap: var(--space-3); box-sizing: border-box;
    width: 100%; min-height: 3rem; padding: 0 var(--space-3);
    border-left: 3px solid transparent; border-bottom: 1px solid var(--line);
    font-size: var(--text-body); letter-spacing: var(--tracking-label); text-transform: uppercase; line-height: 1.2;
    color: var(--ink); text-decoration: none;
  }
  .app-menu__link:hover, .app-menu__row:hover, .app-menu__account .person:hover, .app-menu__account .signout button:hover { background: var(--wash); }
  .app-menu__link[aria-current='page'] { border-left-color: var(--ink); background: var(--wash); }
  .app-menu__link:focus-visible, .app-menu__row:focus-visible, .app-menu__account .person:focus-visible, .app-menu__account .signout button:focus-visible { outline-offset: -2px; }
  /* The person's own things: a rule above, then the person, POS mode, the manual and sign out, in rows as large as the items. */
  .app-menu__list li:last-child .app-menu__link { border-bottom: 0; }
  .app-menu__account { max-width: 40rem; margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--ink); }
  .app-menu__account .person { text-transform: none; letter-spacing: normal; }
  .app-menu__account .person > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .app-menu__row, .app-menu__account .person, .app-menu__account .signout button { text-transform: none; letter-spacing: normal; }
  .app-menu__account .pos-mode-button { display: flex; width: 100%; min-height: 3rem; margin: var(--space-3) 0; }
  .app-menu__account .signout { display: block; margin: 0; }
}
@media (max-width: 23rem) { .app-menu > .app-menu__button { padding: 0 var(--space-3); } }
@media (max-width: 63.99rem) and (min-width: 40rem) {
  /* A tablet standing up: the items in two columns, so the menu is a short panel rather than a tall one. */
  .app-menu__list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-6); }
}
@media (min-width: 64rem) { .app-menu { display: none; } }
@media (forced-colors: active) { .app-menu[open] > .app-menu__button::before { display: none; } }
@media print { .app-menu { display: none; } }

/* ------------------------------------------------------- Page header ----- */

.page-header {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap;
  padding-bottom: var(--space-4); margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--line);
}
.page-header > :first-child { min-width: 0; flex: 1 1 12rem; } /* the title shares its line with the actions, and wraps its own words, before the actions are pushed onto a line of their own */
.page-header h1 { overflow-wrap: anywhere; }
.page-header__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; justify-content: flex-end; margin-left: auto; } /* top right, also when a long title pushes it onto a line of its own */
.page-header__actions > form { margin: 0; display: flex; gap: var(--space-2); align-items: center; }
/* One bar of buttons reads as one set: a single primary, and the rest the same secondary, at one height. A quiet or a small button in the bar is drawn as the others are. */
.page-header__actions .btn--quiet { border-color: var(--ink); background: var(--paper); }
.page-header__actions .btn--quiet:hover { background: var(--wash); }
.page-header__actions .btn--small { min-height: var(--control-height); padding: 0 var(--space-4); font-size: var(--text-small); }
.page-header .lede { margin: var(--space-1) 0 0; }
/* The sentence under the dashboard's greeting (18 October 2026): one line about the person's day, each thing in it a link to its list. */
.page-header .greeting-line { margin: var(--space-2) 0 0; max-width: var(--measure); font-size: var(--text-body); }

.breadcrumbs { font-size: var(--text-small); color: var(--ink-muted); margin-bottom: var(--space-2); }
.breadcrumbs a { text-decoration: none; }
.breadcrumbs a:hover { text-decoration: underline; }
.breadcrumbs span::after { content: '/'; margin: 0 var(--space-2); color: var(--line); }
.breadcrumbs span:last-child::after { content: ''; }

/* --------------------------------------------------------------- Panel --- */

.panel {
  border: 1px solid var(--line);
  background: var(--paper);
  padding: var(--space-6);
  margin-bottom: var(--space-6);
}
.panel > h2 { margin-bottom: var(--space-4); }
.panel--quiet { background: var(--wash); }
/* A part of a panel under its own h3 (the paper record inside an artist's Agreement section, the UI polish pass, 30 October 2026): a rule above it and room, so it reads as a part and not as the next sentence. */
.panel__sub { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--line); }
.panel__sub > h3 { margin-bottom: var(--space-3); }

/* -------------------------------------------------------------- Buttons -- */

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  padding: 0 var(--space-4);
  font: inherit;
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  border-radius: var(--radius);
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duration) var(--ease), color var(--duration) var(--ease);
}
.btn:hover { background: #000; }

.btn--secondary { background: var(--paper); color: var(--ink); border-color: var(--ink); }
.btn--secondary:hover { background: var(--wash); }

/* Quiet: for actions that sit inside a row and must not compete with the data. */
.btn--quiet { background: transparent; color: var(--ink); border-color: var(--line); }
.btn--quiet:hover { border-color: var(--ink); background: transparent; }

.btn--danger { background: var(--danger); border-color: var(--danger); color: var(--paper); }
.btn--danger:hover { background: #7f2418; }

.btn--small { min-height: 2rem; padding: 0 var(--space-3); font-size: var(--text-label); }

/* A small button standing in a row of boxes is the height of the boxes (it was 32px, or 40 on a touch screen, beside boxes of 44). */
.row .btn--small, .search-row .btn--small { min-height: var(--control-height); font-size: var(--text-small); padding: 0 var(--space-4); }

/* A button with a long label (the words are quoted by the manual, so they stay) is set in sentence case on a phone, so it is one line in the width of the screen; the form bar gives it to any label over 24 letters. */
@media (max-width: 40rem) { .btn--long { text-transform: none; letter-spacing: normal; } }

/* The one action a page is for (Sell, on a work's page): taller and wider than the buttons beside it. */
.btn--large { min-height: 3.25rem; padding: 0 var(--space-8); font-size: var(--text-body); }

.btn:disabled, .btn[aria-disabled='true'] {
  opacity: 0.45; cursor: not-allowed; pointer-events: none;
}

.btn[data-loading='true'] { position: relative; color: transparent; pointer-events: none; }
.btn[data-loading='true']::after {
  content: ''; position: absolute; width: 0.9em; height: 0.9em;
  border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
  color: var(--paper);
  animation: spin 600ms linear infinite;
}
.btn--secondary[data-loading='true']::after, .btn--quiet[data-loading='true']::after { color: var(--ink); }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn[data-loading='true']::after { animation-duration: 2s; }
}

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

/* The height of a field's heading: its line, plus the gap under it. Anything
 * that has to line up with the controls rather than the headings drops by
 * this much. */
:root { --field-label-height: calc(var(--text-label) * var(--leading-body) + var(--space-1)); }

.field { margin-bottom: var(--space-4); }
.field:last-child { margin-bottom: 0; }

.field > label {
  display: block;
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 500;
  margin-bottom: var(--space-1);
}

/*
 * Every input the gallery types into, styled by default.
 *
 * Written as an exclusion rather than a list of types, because the list was
 * one: `input[type='text']` and its seven siblings. An attribute selector
 * matches the *attribute*, and `<input name="reorder_level">` has none — the
 * type defaults to text in HTML and the selector still does not match. So 75
 * inputs across ten screens had no width, no padding, no border and no
 * height, and inside a `.field.narrow` they overflowed their own field by
 * 49px and pushed the row off the page.
 *
 * The failure mode matters more than the count. An unstyled input is
 * invisible until somebody opens that one screen and looks, which is exactly
 * how it survived. This way a new input is styled unless it is genuinely a
 * different kind of control, and the ones that are say so.
 */
input:not([type='checkbox'], [type='radio'], [type='file'], [type='submit'],
          [type='button'], [type='reset'], [type='range'], [type='color'],
          [type='image'], [type='hidden']),
select, textarea {
  width: 100%;
  min-height: var(--control-height);
  padding: var(--space-2) var(--space-3);
  font: inherit;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--control-border);
  border-radius: var(--radius);
}
textarea { min-height: 6rem; resize: vertical; line-height: var(--leading-body); }
/* A phone's browser zooms the page in when a box under 16 pixels is pressed (iOS does, and it does not zoom back), so a box is 16 pixels on a phone, a tablet standing up and a touch screen (30 October 2026). */
@media (max-width: 48rem), (pointer: coarse) {
  input:not([type='checkbox'], [type='radio'], [type='file'], [type='submit'],
            [type='button'], [type='reset'], [type='range'], [type='color'],
            [type='image'], [type='hidden']),
  select, textarea { font-size: 1rem; }
}
input:hover, select:hover, textarea:hover { border-color: var(--ink-muted); }
input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--ink); }
/* A box that cannot be typed in is quiet: a pale edge on the wash (it is not asked to meet 3:1), so it does not read as one to fill in. */
input:disabled, select:disabled, textarea:disabled { border-color: var(--line); background: var(--wash); cursor: not-allowed; }
input[readonly]:not([type='checkbox'], [type='radio']), textarea[readonly] { background: var(--wash); }

input[type='checkbox'], input[type='radio'] {
  width: 1.125rem; height: 1.125rem; accent-color: var(--ink); margin-right: var(--space-2);
}
.choice { display: flex; align-items: center; min-height: var(--control-height); }
.choice label { display: flex; align-items: center; text-transform: none; letter-spacing: normal;
  font-size: var(--text-body); color: var(--ink); margin: 0; font-weight: 400; }

.field__help { font-size: var(--text-small); color: var(--ink-muted); margin-top: var(--space-1); }
/*
 * The mark beside a field that must be filled in (`src/ui/required-mark.tsx`, docs/04a-design-system.md Part F). Words in the label, never a symbol and never a colour alone:
 * (required) is darker than the label (the form will not save without it); (needed to go live) is quieter, #666666 (5.7:1 on white, 5.3:1 on the wash), because a draft saves without it.
 * Sentence case and normal spacing, so a label drawn in capitals does not shout them.
 */
.req { font-weight: 400; text-transform: none; letter-spacing: normal; white-space: nowrap; }
.req--required { color: var(--ink); }
.req--live { color: #666666; }
.form-key { font-size: var(--text-small); color: var(--ink-muted); margin: 0 0 var(--space-4); }
.field__error { font-size: var(--text-small); color: var(--danger); margin-top: var(--space-1); }
.field--invalid input, .field--invalid select, .field--invalid textarea { border-color: var(--danger); }

/* Currency and other prefixed inputs. */
.input-prefix { display: flex; align-items: stretch; }
.input-prefix__mark {
  display: flex; align-items: center; padding: 0 var(--space-3);
  border: 1px solid var(--control-border); border-right: 0; background: var(--wash);
  color: var(--ink-muted); font-size: var(--text-small);
}
.input-prefix input { border-left: 0; }

input[type='file'] {
  font-size: var(--text-small); padding: var(--space-2) var(--space-3); border: 1px solid var(--control-border); width: 100%;
}

.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: var(--space-4); }
/* The foot of a form that has fields: its last button, or the paragraph or row of buttons that holds it, is at the right end (the owner's standard, 18 October 2026), as .form-actions and the bar are. */
form[method='post']:has(:is(input:not([type='hidden']), select, textarea)):not(.row, .cluster, .search-row, .inline, .filters, td *) > :is(.btn, button):last-child { display: flex; margin-left: auto; }
form[method='post']:has(:is(input:not([type='hidden']), select, textarea)):not(.row, .cluster, .search-row, .inline, .filters, td *) > :is(p:has(> :is(.btn, button):only-child), .cluster:has(> :is(.btn, button))):last-child { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: var(--space-2); }
.form-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: flex-end; margin-top: var(--space-6); } /* the owner's standard: Save and Cancel at the right end, Save last */

/*
 * The Save and Cancel bar (B11, stages 1 and 2; `src/ui/form-action-bar.tsx`, `docs/forms-save-bar.md`).
 *
 * `position: sticky` inside its own form, as the form's last direct child: it stays at the foot of
 * the screen while any of the form is on the screen and comes to rest under the last field, so
 * nothing is ever covered and a page of several forms has a bar in each. Fixed to the viewport it
 * would hide the last field and could not belong to one form of several. Solid, because fields
 * pass under it; a rule above it and no shadow (the only shadow in the application is the modal's),
 * with a soft fade above so a field running under it reads as running under something.
 * The safe-area padding keeps the buttons off the home indicator (`viewport-fit=cover` in Layout).
 * The buttons stand at the right end, Cancel first and Save last (the owner's standard, 18 October 2026); the note about unsaved changes is at the left.
 */
.form-action-bar {
  position: sticky; bottom: 0; z-index: 20;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-2) var(--space-4);
  margin-top: var(--space-6);
  padding: var(--space-3) var(--space-4);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  padding-left: calc(var(--space-4) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--space-4) + env(safe-area-inset-right, 0px));
  background: var(--paper);
  border-top: 1px solid var(--ink);
}
.form-action-bar::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 100%; height: var(--space-4);
  background: linear-gradient(to top, rgb(20 20 20 / 7%), rgb(20 20 20 / 0%));
  pointer-events: none;
}
.form-action-bar__buttons { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; justify-content: flex-end; margin-left: auto; }
.form-action-bar__status { margin-right: auto; font-size: var(--text-small); color: var(--ink-muted); }
.form-action-bar__status:empty { display: none; }
/* The press is on its way: the button reads Saving… and ignores a second. */
.form-action-bar .btn.is-busy { cursor: progress; }

/* A focused field is scrolled clear of the bar, not left under it. */
html:has([data-form-bar]) { scroll-padding-bottom: 6rem; }

/* A phone: the note sits above the buttons, which are stacked at the full width, each a full-height thumb target, in the order of the markup (Cancel, then Save last, at the foot). */
@media (max-width: 40rem) {
  .form-action-bar { flex-direction: column; align-items: stretch; gap: var(--space-2); }
  .form-action-bar__status { margin-right: 0; }
  .form-action-bar__buttons { flex-direction: column; flex-wrap: nowrap; align-items: stretch; margin-left: 0; }
  html:has([data-form-bar]) { scroll-padding-bottom: 9rem; }
}
@media print { .form-action-bar { display: none; } }

/* The confirmation and the leave-warning (public/form-guard.js): the modal above, with a list. */
dialog.modal.form-guard { max-width: 34rem; }
.form-guard__intro { margin: 0 0 var(--space-3); }
.form-guard__list {
  list-style: none; margin: 0 0 var(--space-3); padding: 0;
  max-height: min(50vh, 24rem); overflow: auto; border-top: 1px solid var(--line);
}
.form-guard__list li { padding: var(--space-2) 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
.form-guard__old, .form-guard__arrow { color: var(--ink-muted); }
.form-guard__masked { color: var(--ink-muted); font-style: italic; }
.form-guard__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-4); }
@media (max-width: 40rem) { .form-guard__actions .btn { flex: 1 1 100%; } }

/*
 * A row of fields.
 *
 * Aligned to the start, so every label in a row begins at the same height and
 * every control sits directly beneath its own label, whatever help or error
 * text hangs below it. This used to align to the end, which had two faults a
 * person sees without being able to name: a field carrying help text pushed
 * its neighbours' controls out of line, and — because .field:last-child drops
 * its bottom margin, and flex-end lines up margin boxes rather than borders —
 * the last field in every row sat exactly one --space-4 lower than the rest.
 * That was on every multi-field row in the application.
 *
 * The row owns the spacing between fields, so a field inside one carries no
 * margin of its own; otherwise the gap between two wrapped lines is the row's
 * gap plus a margin, and the last field's is the row's gap alone.
 */
.row { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-3); align-items: start; }
.row > .field { flex: 1 1 11rem; min-width: min(11rem, 100%); margin-bottom: 0; }

/* A button is as wide as its words. Only fields share out the space. */
.row > .btn, .row > button, .row > a, .row > form { flex: 0 0 auto; }
.row > .field:not(:has(> label)):has(> :is(.btn, button)) { flex: 0 0 auto; min-width: 0; }

/* A field asked to be narrow is narrow: a sort order is two characters wide. */
.row > .field.narrow { flex: 0 0 auto; min-width: 0; }

/* A tick box with its words is as wide as they are, so its label stays on one line beside the boxes of its row (it was cut to two and three lines in a column of 11rem). */
.row > .field:has(> label > input[type='checkbox']) { flex: 0 0 auto; min-width: 0; max-width: 100%; }

/* And one asked to grow takes the slack — a title beside a year. */
.row > .grow { flex: 1 1 11rem; flex-grow: 2; min-width: min(11rem, 100%); }

/* A field in a grid is spaced by the grid, for the same reason as a row. */
.form-grid > .field { margin-bottom: 0; }

/*
 * A tick box is an answer, not a heading, so a label wrapped round one is not
 * dressed as a field label. New markup should use .choice; this catches the
 * pages written before it existed, which is most of them.
 */
.field > label:has(> input[type='checkbox']),
.field > label:has(> input[type='radio']) {
  display: flex; align-items: center;
  text-transform: none; letter-spacing: normal; font-weight: 400;
  font-size: var(--text-body); color: var(--ink);
  min-height: var(--control-height); margin: 0;
}
/* The tick keeps its size beside words that wrap (it was squeezed to half a box beside a line of two). */
.field > label:has(> input[type='checkbox']) > input, .field > label:has(> input[type='radio']) > input { flex: none; }

/*
 * A tick box or a lone button standing in a row of labelled fields drops by
 * the height of a label, so it sits on the line of the controls beside it
 * rather than on the line of their headings.
 *
 * Only where the row actually has headings: the locations editor is a row of
 * bare inputs labelled by aria-label alone, and dropping its Save button
 * would misalign the very thing this fixes elsewhere. "Has headings" is read
 * as "has a label with a control after it", because :has() cannot be nested
 * inside :has() — the first version asked for a label that did not contain a
 * control, which is one :has() too many and made the whole rule invalid.
 */
.row:has(> .field > label:first-child:not(.visually-hidden) + :is(input, select, textarea))
  > .field > label:first-child:has(> input:is([type='checkbox'], [type='radio'])) {
  margin-top: var(--field-label-height);
}
.row:has(> .field > label:first-child:not(.visually-hidden) + :is(input, select, textarea))
  > .field:not(:has(> label)):has(> :is(.btn, button)) {
  margin-top: var(--field-label-height);
}
/* The same for a button that is a child of the row itself, and where the label and its box are a layer down (a customer picker, an input with a prefix). A label that is not drawn (visually hidden: a row's list named only for a screen reader) is no heading to drop under. */
.row:has(label:not(.visually-hidden) + :is(input, select, textarea, .input-prefix, .picker__search)) > :is(.btn, button),
.row:has(label:not(.visually-hidden) + :is(input, select, textarea, .input-prefix, .picker__search)) > .field:not(:has(> label)):has(> :is(.btn, button)) {
  margin-top: var(--field-label-height);
}

/* --------------------------------------------------------------- Filters -- */

.filters {
  border: 1px solid var(--line);
  background: var(--wash);
  padding: var(--space-4);
  margin-bottom: var(--space-6);
  display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-4) var(--space-3); align-items: start;
}
.filters .field { margin-bottom: 0; }

/* The grid is aligned to the start for the reason rows are: the search field carries a line of help under it, and end-alignment lifted its box a whole line above the controls beside it. */
.filters__search { grid-column: span 2; min-width: 16rem; }
@media (max-width: 40rem) { .filters__search { grid-column: span 1; min-width: 0; } }

/*
 * What a filter panel says about itself (the note under the search box that says how many names it offers) is a line of the panel, not of the first column: it
 * is the last thing in the form and takes the whole width, so it never wraps into a strip beside the controls and never pushes the second row out of line.
 */
.filters > .field__help { grid-column: 1 / -1; margin: 0; padding-top: var(--space-3); border-top: 1px solid var(--line); }

/*
 * A panel with many filters can ask for its rows to be rows: the same number of equal columns in each, the search box two of them, the Search button after the last
 * filter on the last row. Without it the grid fits as many columns as there are 11rem to spare and the second row begins wherever the first one stopped.
 */
@media (min-width: 80rem) { .filters--aligned { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (min-width: 48rem) and (max-width: 79.99rem) { .filters--aligned { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/*
 * Every box of a filter panel sits on one line of its row, whatever its neighbours hold (19 October 2026, after the Artworks and Mediums panels). Each rule below is a fault
 * that was on a screen: a heading that wrapped and pushed its box a line lower, a boxed group with a legend that sat lower and taller than the boxes beside it, a button or a tick
 * box standing on the line of the headings, and two tick boxes on two lines. The panel's look is unchanged (the same tokens, the same sizes); only where things stand.
 */

/* A heading is one line. One too long for its column is cut, never wrapped: a wrapped heading pushed its box down by a line. Name a filter in a word or two. */
.filters .field > label:not(:has(> input)) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }

/*
 * Where a filter panel's buttons stand (the owner's standard, 18 October 2026; `FilterActions`, src/ui/filter-actions.tsx): the last row of the panel, at its right end, the
 * primary (Search, Filter) rightmost and Clear to its left. On a phone the two share the width, each a thumb target.
 */
.filters__actions { grid-column: 1 / -1; justify-self: end; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
@media (max-width: 40rem) { .filters__actions { justify-self: stretch; } .filters__actions > .btn { flex: 1 1 0; } }

/*
 * A search of one box, or one line of boxes, with its button: `<form class="search-row">`. The boxes grow, the button is as wide as its words, at the right end of the line, on the
 * line of the boxes and at their height (44px, as a button is). A label above a box drops the button by the height of a label, so it stands on the box and not on the heading.
 */
.search-row { display: flex; align-items: flex-start; gap: var(--space-3); }
.search-row > :is(.field, .grow) { flex: 1 1 11rem; min-width: 0; margin: 0; }
/* A search that is a card of its own on the page (the Vouchers' find by code, the attendees' search) is drawn as a filter panel is. */
.search-row--panel { border: 1px solid var(--line); background: var(--wash); padding: var(--space-4); margin-bottom: var(--space-6); }
.panel > .search-row { margin-bottom: var(--space-4); }
.panel > .search-row:last-child { margin-bottom: 0; }
.search-row > input { flex: 1 1 8rem; width: auto; min-width: 0; }
.search-row > select, .search-row > .field:has(> select) { flex: 0 1 22rem; width: auto; min-width: 0; }
/* A line of several boxes is a line from 40rem up; below that the boxes take a line each and the button the last, at the right. One box that grows (a find box) is not several: its button stays beside it. Several = a later box with a heading of its own; :has() cannot hold another :has(), which is why this reads the heading as a child (`> label`). */
@media (max-width: 40rem) {
  .search-row:has(> :is(.field, .grow) ~ :is(.field, .grow) > label) { flex-wrap: wrap; }
  .search-row:has(> :is(.field, .grow) ~ :is(.field, .grow) > label) > :is(.field, .grow) { flex: 1 1 100%; }
  .search-row:has(> :is(.field, .grow) ~ :is(.field, .grow) > label) > :is(.btn, button, .field:not(:has(> label))) { flex: none; margin-left: auto; margin-top: 0; }
}
.search-row > .field:has(> select) > select { width: 100%; }
.search-row > :is(.btn, button), .search-row > .field:not(:has(> label)):has(> :is(.btn, button)) { flex: none; margin-left: 0; }
.search-row:has(label:not(.visually-hidden)) > :is(.btn, button),
.search-row:has(label:not(.visually-hidden)) > .field:not(:has(> label)):has(> :is(.btn, button)) { margin-top: var(--field-label-height); }
/* Two selects on one line share it; two buttons stand together at the end. */
.search-row > select:has(~ select), .search-row > select ~ select, .search-row > .field:has(> select):has(~ .field > select), .search-row > .field:has(> select) ~ .field:has(> select) { flex: 1 1 10rem; }

/* A row of tick boxes (choices): side by side, wrapping together, each as tall as a box. In a filter panel it stands on the line of the boxes beside it. */
.check-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--space-4); }
.check-row > label { display: inline-flex; align-items: center; min-height: var(--control-height); text-transform: none; letter-spacing: normal; font-weight: 400; font-size: var(--text-body); color: var(--ink); margin: 0; }
.filters > .check-row { align-self: end; } /* on the line of the boxes beside it (the bottom of the row), with no margin to count as a gap */
.field > .check-row { margin-top: var(--space-1); }

/* A tick box with a long sentence takes two columns rather than wrapping to three lines in one. */
.filters__wide { grid-column: span 2; }
@media (max-width: 40rem) { .filters__wide { grid-column: span 1; } }

/*
 * A group of choices under a legend (a `<fieldset class="field">`, and any fieldset in a filter panel) is a field like the others: no border, no padding, no inset, the legend in the same type
 * as a heading and above its choices. (A fieldset's own box sat lower and taller than the boxes beside it, in a row, a grid and a filter panel.)
 */
.filters fieldset, fieldset.field { border: 0; padding: 0; min-width: 0; }
.filters fieldset { margin: 0; }
fieldset.field { margin-inline: 0; margin-top: 0; }
:is(.filters fieldset, fieldset.field) > legend:not(.visually-hidden) {
  float: left; width: 100%; padding: 0; margin: 0 0 var(--space-1);
  font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); font-weight: 500;
}
:is(.filters fieldset, fieldset.field) > legend:not(.visually-hidden) + * { clear: both; }

/* --------------------------------------------------------------- Tables -- */

/*
 * The card a table sits in. It scrolls the table sideways inside its own border, so a wide table never widens the page; `min-width: 0` lets it shrink when
 * it is the child of a flex or grid box; and the shading at either edge, drawn only while there is more to see that way, says that it scrolls (a scroll bar
 * at the foot of a long table is a hundred rows away).
 */
.table-wrap {
  max-width: 100%; min-width: 0; overflow-x: auto; border: 1px solid var(--line);
  background:
    linear-gradient(to right, var(--paper) 30%, rgb(255 255 255 / 0%)) left center / 2rem 100% no-repeat local,
    linear-gradient(to left, var(--paper) 30%, rgb(255 255 255 / 0%)) right center / 2rem 100% no-repeat local,
    linear-gradient(to right, rgb(20 20 20 / 14%), rgb(20 20 20 / 0%)) left center / 0.75rem 100% no-repeat scroll,
    linear-gradient(to left, rgb(20 20 20 / 14%), rgb(20 20 20 / 0%)) right center / 0.75rem 100% no-repeat scroll,
    var(--paper);
}
/* A card that is a panel with a table straight in it (older screens) does the same, so it is the card that scrolls and not the page. */
.panel:has(> table), .panel:has(> form > table) { max-width: 100%; min-width: 0; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }

th, td { text-align: left; padding: var(--space-3); vertical-align: middle; }
thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--wash);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 500;
  white-space: nowrap;
}
tbody tr { border-bottom: 1px solid var(--line); }
tbody tr:last-child { border-bottom: 0; }
tbody tr:hover { background: var(--wash); }
/* A row with a box to type or choose in is not washed under the pointer: the box's edge is 3:1 against the page, and 2.98:1 against the wash of an accent colour. */
tbody tr:hover:has(textarea, select, input:not([type='checkbox'], [type='radio'], [type='hidden'], [type='submit'], [type='button'])) { background: transparent; }

th a { text-decoration: none; color: inherit; display: inline-flex; gap: var(--space-1); }
th a:hover { color: var(--ink); }
th[aria-sort] a { color: var(--ink); }

/*
 * A table's headings, pressed to sort it (6 November 2026 for the reports, 7 November 2026 for the staff lists; public/report-sort.js, src/ui/sortable-head.tsx).
 *
 * The heading is a real button inside the th, so the keyboard works and the global focus ring is drawn round it. It is written as plain heading text first (same size, weight, case, colour
 * and alignment: a money or numeric column stays right-aligned because the button takes its th's), and only a table the script has marked `is-sortable` gets the pointer, the hover and the
 * arrow, so with no script a page looks exactly as it did. The arrow is drawn from the th's aria-sort and is not text, so it is not read twice and not printed: up for low to high, down for high to low.
 * In a right-aligned column the arrow stands before the words, so the words still end where the figures do.
 */
.sort-button {
  display: inline-flex; align-items: center; gap: var(--space-1);
  margin: 0; padding: 0; border: 0; border-radius: 0; background: none;
  font: inherit; letter-spacing: inherit; text-transform: inherit; text-align: inherit; white-space: inherit; color: inherit; cursor: inherit;
}
th.money .sort-button { flex-direction: row-reverse; }
.sort-arrow { display: none; }
.is-sortable .sort-button { cursor: pointer; }
.is-sortable .sort-button:hover, .is-sortable th[aria-sort] .sort-button { color: var(--ink); }
.is-sortable .sort-arrow { display: inline-block; width: 1em; text-align: center; font-size: 0.8em; line-height: 1; }
.is-sortable .sort-arrow::before { content: '\2195'; }
.is-sortable th[aria-sort='ascending'] .sort-arrow::before { content: '\25B2'; }
.is-sortable th[aria-sort='descending'] .sort-arrow::before { content: '\25BC'; }
/* A touch screen wider than the card layout (a tablet lying down) gets the whole height of the heading to press, with no taller row: the padding the button takes is the th's own. */
@media (pointer: coarse) {
  .is-sortable .sort-button { padding-block: var(--space-3); margin-block: calc(-1 * var(--space-3)); }
}
/* The phone's "Sort by" box (the table is cards there and its heading row is hidden): made by the script, above the table, in the look of the filter boxes (.field, a label over a select), and drawn only where the table turns into cards. */
.table-sort-by { display: none; }
@media (max-width: 60rem) {
  .table-sort-by { display: flex; gap: var(--space-3); margin-bottom: var(--space-4); }
  .table-sort-by > .field { flex: 1 1 0; min-width: 0; margin: 0; }
}
@media print {
  .sort-arrow, .table-sort-by { display: none !important; }
  .sort-button { padding: 0; margin: 0; }
}

.table-empty { padding: var(--space-12) var(--space-6); text-align: center; color: var(--ink-muted); }

/* A key/value table inside a panel: no hover, label column quiet. */
.table--detail tbody tr:hover { background: transparent; }
.table--detail th {
  position: static; background: transparent;
  width: 12rem; vertical-align: top; padding-left: 0;
}
/* On a phone a key and its value are one above the other: a key column of 14rem left the value a few characters. */
@media (max-width: 40rem) { table.table--detail th, table.table--detail td { display: block; width: auto; padding-left: 0; } table.table--detail th { border-bottom: 0; padding-bottom: 0; } table.table--detail tbody, table.table--detail tr { display: block; } }
/* The separator goes on the cells, not the row. Under border-collapse a row
   border paints unevenly across cells, which reads as though every value has
   been underlined. */
.table--detail tbody tr { border-bottom: 0; }
.table--detail th, .table--detail td { border-bottom: 1px solid var(--line); }
.table--detail tbody tr:last-child th, .table--detail tbody tr:last-child td { border-bottom: 0; }

/* Narrow screens: rows become stacked records with their labels shown. */
@media (max-width: 60rem) {
  /* A table that is drawn as stacked records has no columns: fixed column widths (a colgroup, table-layout: fixed) would squeeze the records into the first one. */
  .table--collapse { table-layout: auto !important; }
  .table--collapse colgroup { display: none; }
  .table--collapse, .table--collapse tbody { display: block; } /* the records take the width of the card, not the width of their longest line */
  .table--collapse thead { display: none; }
  .table--collapse tbody tr { display: block; padding: var(--space-3) 0; }
  .table--collapse td { display: flex; justify-content: space-between; gap: var(--space-4);
    padding: var(--space-1) var(--space-3); border: 0; text-align: right; }
  .table--collapse td[data-label='Image'] { display: none; }
  .table--collapse td::before {
    content: attr(data-label);
    font-size: var(--text-label); letter-spacing: var(--tracking-label);
    text-transform: uppercase; color: var(--ink-muted); text-align: left; flex: none;
  }
  /* A cell with no label is a button, not a value with its name missing.
     Ranged left with the row's other labels rather than floated against an
     empty gutter, which is what it looked like. */
  .table--collapse td:not([data-label]) {
    justify-content: flex-start;
    padding-top: var(--space-2);
  }
  .table--collapse td:not([data-label])::before { content: none; }

  /*
   * A cell holding several lines, not one value.
   *
   * `td` is a flex row here so that a label can sit in the gutter with its
   * value ranged right, and that is right for "Postcode: 0624". A cell with
   * four lines in it becomes four flex items side by side and runs off the
   * edge of the screen -- which is what the customer import's "what was
   * tidied" column did on a tablet, and the only way to find it was to look.
   *
   * Marked rather than automatic: the two-child cells everywhere else read
   * correctly as a value with its detail beside it, and changing those would
   * be a redesign rather than a fix.
   */
  .table--collapse td.stacked { display: block; text-align: left; }
  .table--collapse td.stacked::before { display: block; margin-bottom: var(--space-1); }

  /*
   * A detail row is a full-width block, not a labelled value.
   *
   * Left to the rules above it became a flex row with a label in the gutter
   * and its content ranged right, which pushed a block of courier JSON off
   * the side of the phone entirely.
   */
  .table--collapse .detail-row > td {
    display: block; text-align: left; padding: 0 var(--space-3);
  }
  .table--collapse .detail-row > td::before { content: none; }

  /*
   * Capped against the viewport, not the cell.
   *
   * `max-width: 100%` resolves against a cell the table has already grown to
   * fit, so a wide block of JSON drives the table wider than its wrapper and
   * pushes every value in every row off the side. The viewport cannot be
   * driven by the content, so it is the only honest thing to measure against
   * here.
   */
  .table--collapse .detail-row pre { max-width: calc(100vw - 7rem); }
}

/* --------------------------------------------------------------- Badges -- */
/* Colour is never the only signal: each badge carries its own word, and the
   dot shape differs by state for anyone who cannot separate the hues. */

.badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  font-size: var(--text-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; padding: 0.15rem var(--space-2);
  border: 1px solid var(--line); background: var(--paper); color: var(--ink-muted);
  white-space: nowrap;
}
.badge::before { content: '\25CB'; font-size: 0.7em; line-height: 1; }

.badge--available { color: var(--ok); border-color: var(--ok); background: var(--ok-wash); }
.badge--available::before { content: '\25CF'; }

.badge--on-hold { color: var(--warn); border-color: var(--warn); background: var(--warn-wash); }
.badge--on-hold::before { content: '\25D0'; }

.badge--sold { color: var(--ink); border-color: var(--ink); background: var(--ink); }
.badge--sold { color: var(--paper); }
.badge--sold::before { content: '\25CF'; }

.badge--draft { color: var(--ink-muted); border-style: dashed; }
.badge--draft::before { content: '\25CC'; }

.badge--returned, .badge--withdrawn { color: var(--ink-muted); background: var(--wash); }
.badge--returned::before, .badge--withdrawn::before { content: '\2014'; }

/*
 * Going back to the artist: still here, already off sale. Warm like `on-hold`
 * because both mean "do not sell this", and its own glyph -- an outlined arrow
 * rather than a filled dot -- so the statuses stay distinguishable in
 * greyscale, in print, and to anyone who cannot separate the hues.
 */
.badge--returning { color: var(--warn); border-color: var(--warn); border-style: dashed; }
.badge--returning::before { content: '\21A9'; }

/* A person who has never traded (one customer record): a dashed outline. The same word in the Customers list and in every picker that names a customer. */
.badge--contact { color: var(--ink-muted); border-style: dashed; }
.badge--contact::before { content: '\25CC'; }

/* A photograph too small to use: a warning rather than a state, so the warm
   colour without the dashed edge, and a square of its own for greyscale. */
.badge--low-res { color: var(--warn); border-color: var(--warn); }
.badge--low-res::before { content: '\25AB'; }

/* Something staff should look at before going on: a work flagged Special
   shipping, a bank account that differs, a check-in overdue. Warm, with a
   triangle of its own so it reads in greyscale. */
.badge--warn { color: var(--warn); border-color: var(--warn); background: var(--warn-wash); }
.badge--warn::before { content: '\25B2'; }

/* Damaged, or a condition report that is major and still open: stops a sale. */
.badge--danger { color: var(--danger); border-color: var(--danger); background: var(--danger-wash); }
.badge--danger::before { content: '\25B2'; }

/* A set (Diptych, Triptych, Set of N) or a part of one (B11 stage 6): a quiet badge beside the title with a double rule of its own, so it reads in greyscale and is never
   mistaken for a status. The family's reason under a status is a small italic line. */
.badge--set { color: var(--ink); border-style: double; border-width: 3px; background: var(--sage-wash); }
.badge--set::before { content: '\25A4'; }
.family-note { color: var(--ink-muted); font-style: italic; }

/* -------------------------------------------------------------- Notices -- */

.notice {
  border: 1px solid var(--line); border-left-width: 3px;
  padding: var(--space-3) var(--space-4); margin-bottom: var(--space-6);
  font-size: var(--text-small);
}
.notice--ok { border-left-color: var(--ok); background: var(--ok-wash); color: var(--ok); }
.notice--error { border-left-color: var(--danger); background: var(--danger-wash); color: var(--danger); }
.notice--warn { border-left-color: var(--warn); background: var(--warn-wash); color: var(--warn); }
/* What a saved draft still needs, or the question about making it available (`src/ui/go-live-panel.tsx`): words in ink, the notice's edge says which it is. The buttons stand at the right, Not now first and Yes last. */
.go-live { color: var(--ink); }
.go-live p { margin: 0 0 var(--space-2); }
.go-live ul { margin: 0; padding-left: var(--space-5); }
.go-live li + li { margin-top: var(--space-1); }
.go-live__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: flex-end; align-items: center; margin-top: var(--space-3); }
.go-live__actions form { margin: 0; }
.go-live-line { margin-top: var(--space-1); }
.go-live-line--ready { color: var(--ok); }

/* The question at a confirm step that also sends an email (Batch 10, pass G): one line a message,
   with the Don't send box beside it. A quiet box, not a warning: sending is the ordinary answer. */
.email-prompt {
  border: 1px solid var(--line); border-left: 3px solid var(--ink); background: var(--wash);
  padding: var(--space-3) var(--space-4); margin: var(--space-4) 0; font-size: var(--text-small);
}
.email-prompt__line { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-2) var(--space-4); }
.email-prompt__line + .email-prompt__line { margin-top: var(--space-2); }
.email-prompt p { margin: 0; }
.email-prompt .check { margin: 0; }

/* A message held and not sent, shown on the record it is about with what it said (pass G). */
.held-message { border: 1px solid var(--line); border-left: 3px solid var(--warn); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }
.held-message__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-3); }

/* Whether an invoice's or a quote's email went (B14 item 7, `src/ui/email-status.tsx`): a large panel that stays on the record, read from the email log. The mark and the edge say
   which it is (a tick, a clock, a warning), and the words say it too, so colour is never the only signal. A failure is red and never a success. */
.email-status { border-left: 6px solid var(--line); }
.email-status--ok { border-left-color: var(--ok); background: var(--ok-wash); }
.email-status--wait { border-left-color: var(--warn); background: var(--warn-wash); }
.email-status--bad { border-left-color: var(--danger); background: var(--danger-wash); }
.email-status__main { display: flex; align-items: flex-start; gap: var(--space-4); }
.email-status__mark {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 3.25rem; height: 3.25rem; border-radius: 50%; color: #fff; background: var(--ink-muted);
  font-size: 2rem; font-weight: 700; line-height: 1;
}
.email-status--ok .email-status__mark { background: var(--ok); }
.email-status--wait .email-status__mark { background: var(--warn); }
.email-status--bad .email-status__mark { background: var(--danger); }
.email-status__words { min-width: 0; }
.email-status__words p { margin: 0; }
.email-status__kicker { font-size: var(--text-label); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted); }
.email-status__headline { font-size: var(--text-h2); font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; color: var(--ink); }
.email-status__words .email-status__detail { margin-top: var(--space-1); color: var(--ink); }
.email-status__history { margin-top: var(--space-4); font-size: var(--text-small); }
.email-status__history summary { cursor: pointer; min-height: 2.75rem; display: flex; align-items: center; }
.email-status__history ul { margin: var(--space-2) 0 0; padding-left: var(--space-5); }
.email-status__history li + li { margin-top: var(--space-1); }
.email-status__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-4); }
.email-status__actions .btn { min-height: 2.75rem; }
/* The small mark in a list beside a quote or a sale. */
.emailed-tick { display: inline-flex; align-items: baseline; gap: var(--space-1); font-size: var(--text-small); white-space: nowrap; }
.emailed-tick--emailed { color: var(--ok); }
.emailed-tick--waiting { color: var(--warn); }
.emailed-tick--not_sent { color: var(--danger); font-weight: 600; }
/* What became of the customer's receipt, above the buttons that email one (11 November 2026, `src/ui/receipt-sent.tsx`), read from the email log: a tick (or a clock, or a warning) and the
   words on a wide edge, so it is not missed. The mark, the edge and the words each say which it is, so colour is never the only signal, and a receipt that was held or did not go is never
   drawn as a success. No fixed width: it wraps on a phone. (The till draws the same sentence with its own notice, `.till-notice`.) */
.receipt-sent { border-left: 6px solid var(--line); background: var(--wash); padding: var(--space-3) var(--space-4); margin: 0 0 var(--space-4); text-align: left; }
.receipt-sent--ok { border-left-color: var(--ok); background: var(--ok-wash); }
.receipt-sent--wait { border-left-color: var(--warn); background: var(--warn-wash); }
.receipt-sent--bad { border-left-color: var(--danger); background: var(--danger-wash); }
.receipt-sent__headline { display: flex; align-items: baseline; gap: var(--space-3); margin: 0; font-weight: 600; line-height: 1.35; color: var(--ink); }
.receipt-sent__mark { flex: none; font-weight: 700; }
.receipt-sent--ok .receipt-sent__mark { color: var(--ok); }
.receipt-sent--wait .receipt-sent__mark { color: var(--warn); }
.receipt-sent--bad .receipt-sent__mark { color: var(--danger); }
.receipt-sent__words { min-width: 0; overflow-wrap: anywhere; }
.receipt-sent__list { margin: var(--space-2) 0 0; padding-left: var(--space-8); color: var(--ink); }
.receipt-sent__list li { overflow-wrap: anywhere; }
.receipt-sent__list li + li { margin-top: var(--space-1); }
/* The address on the invoice's confirm step: large, and it breaks rather than widening the page. */
.email-confirm__address { font-size: var(--text-h3); font-weight: 600; overflow-wrap: anywhere; }

/* The pause (pass G): large and plain, because it changes what every message in Flag does. */
.pause-banner {
  border: 2px solid var(--danger); background: var(--danger-wash);
  padding: var(--space-4) var(--space-6); margin-bottom: var(--space-6);
}
.pause-banner strong { display: block; font-size: 1.25rem; margin-bottom: var(--space-2); color: var(--danger); }
.pause-banner p { margin: 0 0 var(--space-2); color: var(--ink); }
.pause-banner a { color: var(--ink); font-weight: 600; }
@media (max-width: 40rem) {
  .pause-banner { padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }
  .pause-banner strong { font-size: 1.125rem; }
  .notice { margin-bottom: var(--space-4); }
}

/* A hold belonging to somebody else, shown on the sale line it is about.
   Not a notice: it sits inside a table cell and has to stay legible at the
   small size the rest of that cell uses. The glyph carries the meaning as
   well as the colour, for the same reason the status badges do. */
.warn-text { color: var(--warn); }
.notice--info { border-left-color: var(--sage); background: var(--sage-wash); color: var(--ink); }

/* A customer's standing VIP discount (batch 4): the gallery's alone, so it is
   told apart from an ordinary discount wherever the till shows money. A deep
   plum, well past AA on paper and clear of the ok/warn/danger states. */
:root { --vip: #5a2d6e; --vip-wash: #f4eef6; }
.vip-discount { color: var(--vip); }
.badge--vip { color: var(--vip); border-color: var(--vip); background: var(--vip-wash); }
.notice--vip { border-left-color: var(--vip); background: var(--vip-wash); color: var(--vip); }

/* --------------------------------------------------------------- Cards --- */
/* Image first, chrome last. The card has no border and no shadow: the image
   is its own edge, exactly as on the gallery's own grid. */

/* ------------------------------------------------------ Figures table -- */

/* Periods down the side, money across. Right-aligned and tabular so the
   columns of digits line up and can be compared by eye. */
/* The row label only. Scoped to the body because an unscoped `th` rule beats
   `.money` on specificity, which left the "Value" and "On lay-by" headings
   ranged left over columns of right-ranged figures — so the heading did not
   sit above the numbers it named. */
.table--figures tbody th { text-align: left; font-weight: 400; }
.table--figures td.money { font-variant-numeric: tabular-nums; white-space: nowrap; }
.table--figures tbody tr:first-child td { font-size: var(--text-large, 1.0625rem); }
/*
 * A figures table whose labels carry words under them (Month end's gift vouchers, 31 October 2026). The amount's cell never wraps, so every sentence goes under its label, in the label's cell, where
 * it wraps; the amount sits level with the label's first line, and a sentence keeps to a readable line however wide the card is. It had put the sentences in the amount's cell, one unbroken line
 * each, which squashed the labels and made the card scroll sideways.
 */
.table--figures.table--explained tbody th, .table--figures.table--explained tbody td { vertical-align: top; }
.table--figures.table--explained th > .muted { max-width: 62ch; margin-top: var(--space-1); }

/* ------------------------------------------------------- Dashboard stats -- */

/* Three figures a manager wants before anything else: what came in today,
   what is being held, what is still owed. Each one is a link to the page
   that acts on it, so the number is a way in rather than an ornament. */
.stat-row {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  margin-bottom: var(--space-6);
}
@media (min-width: 40.01rem) and (max-width: 60rem) { .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } } /* two to a row on a tablet: a label such as "At or below reorder level" is on one line */
.stat {
  display: block; text-decoration: none; color: inherit;
  border: 1px solid var(--line);
  background: var(--paper);
  padding: var(--space-4) var(--space-6);
}
.stat:hover { background: var(--wash); }
.stat:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.stat__label {
  display: block;
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
}
.stat__value {
  display: block;
  font-size: var(--text-display);
  line-height: 1.1;
  margin-top: var(--space-2);
  font-variant-numeric: tabular-nums;
}
.stat__note {
  display: block;
  font-size: var(--text-small);
  color: var(--ink-muted);
  margin-top: var(--space-1);
}
.stat__note.is-danger { color: var(--danger); }

.card-grid {
  display: grid; gap: var(--space-6);
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}
.card { display: block; text-decoration: none; color: inherit; }
.card__media {
  background: var(--wash); aspect-ratio: 4 / 5; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.card__media img { width: 100%; height: 100%; object-fit: cover; }
.card__media--empty { color: var(--ink-muted); font-size: var(--text-small); }
.card__body { padding-top: var(--space-3); }
.card__title { font-size: var(--text-body); }
.card__meta { font-size: var(--text-small); color: var(--ink-muted); }
.card__price { font-variant-numeric: tabular-nums; margin-top: var(--space-1); }
.card:hover .card__title { text-decoration: underline; text-underline-offset: 0.2em; }

.thumb { width: 3rem; height: 3rem; object-fit: cover; background: var(--wash); }
.thumb--empty { display: block; }

/* The parts of a set as small cards, on the set's own screen and a part's (B11 stage 6). */
.part-cards { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); margin-top: var(--space-3); }
.part-card { display: flex; flex-direction: column; gap: var(--space-2); border: 1px solid var(--line); background: var(--paper); padding: var(--space-2); }
.part-card--current { border-color: var(--ink); }
.part-card__media { display: flex; align-items: center; justify-content: center; aspect-ratio: 4 / 5; background: var(--wash); overflow: hidden; color: var(--ink-muted); }
.part-card__media img { width: 100%; height: 100%; object-fit: cover; }
.part-card__title { font-size: var(--text-body); font-weight: 600; text-decoration: none; color: inherit; }
.part-card__title:hover { text-decoration: underline; text-underline-offset: 0.2em; }
.part-card__price { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- Modal, drawer --- */

dialog.modal {
  border: 1px solid var(--ink); border-radius: var(--radius);
  padding: var(--space-6); max-width: 30rem; width: calc(100% - var(--space-8));
  box-shadow: var(--shadow-modal); color: var(--ink); background: var(--paper);
}
dialog.modal::backdrop { background: rgb(20 20 20 / 40%); }
dialog.modal[open] { animation: modal-in var(--duration) var(--ease); }
@keyframes modal-in { from { opacity: 0; transform: translateY(4px); } }
.modal__header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--space-4); }

dialog.drawer {
  border: 0; border-left: 1px solid var(--line); border-radius: 0;
  margin: 0 0 0 auto; height: 100%; max-height: 100%;
  width: min(30rem, 100%); padding: var(--space-6);
  box-shadow: var(--shadow-modal);
}
dialog.drawer::backdrop { background: rgb(20 20 20 / 40%); }

/*
 * The image viewer: the modal above, taken to the edges of the screen. Dark
 * behind the picture so its edges read, with the header on paper so the
 * figures and buttons look like the rest of the application.
 *
 * Fitted, the stage centres the picture and hides overflow. At actual size it
 * becomes a plain block that scrolls: a flex container centring something
 * wider than itself would cut off its left edge where no scrollbar reaches.
 */
dialog.modal.lightbox {
  max-width: none; width: 100vw; height: 100vh; max-height: none;
  margin: 0; padding: 0; border: 0; border-radius: 0;
  background: var(--ink); color: var(--paper);
}
dialog.modal.lightbox[open] { display: flex; flex-direction: column; }
.lightbox__bar {
  display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; justify-content: space-between;
  padding: var(--space-3) var(--space-4); background: var(--paper); color: var(--ink);
  border-bottom: 1px solid var(--line);
}
.lightbox__meta {
  margin: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  font-variant-numeric: tabular-nums;
}
.lightbox__actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.lightbox__actions form { margin: 0; }
.lightbox__stage {
  flex: 1; min-height: 0; overflow: hidden; position: relative;
  display: flex; align-items: center; justify-content: center;
}
.lightbox__stage img { max-width: 100%; max-height: 100%; object-fit: contain; cursor: zoom-in; }
.lightbox.is-actual .lightbox__stage { display: block; overflow: auto; }
.lightbox.is-actual .lightbox__stage img { max-width: none; max-height: none; margin: 0 auto; display: block; cursor: zoom-out; }
.lightbox__loading {
  position: absolute; inset: auto 0 var(--space-4); margin: 0; text-align: center;
  font-size: var(--text-small); color: var(--paper); pointer-events: none;
}

/*
 * The image editor (public/image-editor.js, 3 October 2026): the lightbox's
 * full-screen modal, with the picture on the left and the controls beside it,
 * stacked under it on a phone. The crop box and its eight handles are plain
 * elements over a canvas. `touch-action: none` on the stage, the frame and
 * the box is what lets a finger drag a handle instead of scrolling the page,
 * and each handle has a hit area well beyond what is drawn, because a
 * fingertip is not a pixel.
 */
dialog.modal.image-editor {
  max-width: none; width: 100vw; height: 100vh; max-height: none;
  margin: 0; padding: 0; border: 0; border-radius: 0;
  background: var(--paper); color: var(--ink);
}
dialog.modal.image-editor[open] { display: flex; flex-direction: column; }
.image-editor__bar {
  display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; justify-content: space-between;
  padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--line);
}
.image-editor__title { margin: 0; font-size: var(--text-section); font-weight: 500; overflow-wrap: anywhere; }
.image-editor__actions { display: flex; gap: var(--space-2); align-items: center; }
.image-editor__body { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 18rem; }
.image-editor__stage {
  background: var(--ink); min-height: 0; min-width: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center; touch-action: none;
}
.image-editor__frame { position: relative; touch-action: none; user-select: none; -webkit-user-select: none; }
.image-editor__canvas { display: block; }
.image-editor__crop {
  position: absolute; box-sizing: border-box; cursor: move; touch-action: none;
  border: 2px solid var(--paper); outline: 1px solid rgb(20 20 20 / 60%);
}
.image-editor__crop:focus-visible { outline: 3px solid var(--sage); }
.image-editor__crop.is-dragging { border-color: var(--sage); }
.image-editor__handle {
  position: absolute; width: 14px; height: 14px; box-sizing: border-box;
  background: var(--paper); border: 1px solid var(--ink); touch-action: none;
}
.image-editor__handle::before { content: ''; position: absolute; inset: -15px; }
.image-editor__handle--nw { top: -8px; left: -8px; cursor: nwse-resize; }
.image-editor__handle--n { top: -8px; left: calc(50% - 7px); cursor: ns-resize; }
.image-editor__handle--ne { top: -8px; right: -8px; cursor: nesw-resize; }
.image-editor__handle--e { top: calc(50% - 7px); right: -8px; cursor: ew-resize; }
.image-editor__handle--se { bottom: -8px; right: -8px; cursor: nwse-resize; }
.image-editor__handle--s { bottom: -8px; left: calc(50% - 7px); cursor: ns-resize; }
.image-editor__handle--sw { bottom: -8px; left: -8px; cursor: nesw-resize; }
.image-editor__handle--w { top: calc(50% - 7px); left: -8px; cursor: ew-resize; }
.image-editor__controls {
  padding: var(--space-4); border-left: 1px solid var(--line);
  overflow: auto; display: grid; gap: var(--space-4); align-content: start;
}
.image-editor__row { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.image-editor__label {
  flex-basis: 100%; font-size: var(--text-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--ink-muted);
}
.image-editor__row input[type=range] { flex: 1; min-width: 8rem; accent-color: var(--ink); }
.image-editor__value { font-variant-numeric: tabular-nums; min-width: 3.5rem; text-align: right; }
.image-editor__shapes { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.image-editor__shapes .btn[aria-pressed="true"],
.image-editor__row .btn[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.image-editor__readout { margin: 0; font-size: var(--text-small); font-variant-numeric: tabular-nums; }
.image-editor__readout.is-low { color: var(--warn); }
.image-editor__warn { margin: 0; font-size: var(--text-small); color: var(--warn); }
.image-editor__intro { margin: 0 0 var(--space-4); color: var(--ink-muted); }
/* `.btn` sets its own display, which would otherwise win over the hidden attribute. */
.image-editor [hidden], .image-queue [hidden], .image-queue[hidden] { display: none !important; }
/* A form with a locked shape (data-image-lock): the one shape and the sentence that says where it is shown, in place of the shape buttons. */
.image-editor__lock { flex-basis: 100%; margin: 0; font-size: var(--text-small); color: var(--ink-muted); }
.image-editor__bulkbody { flex: 1; min-height: 0; overflow: auto; padding: var(--space-4); }
@media (max-width: 52rem) {
  .image-editor__body { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; }
  .image-editor__controls { border-left: 0; border-top: 1px solid var(--line); max-height: 45vh; }
}

/* Every picture of a work, before and after, for Rotate all and Apply the same crop. */
.image-bulk { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-bottom: var(--space-4); }
.image-bulk__cell {
  margin: 0; border: 1px solid var(--line); padding: var(--space-2);
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); width: 20rem; max-width: 100%;
}
.image-bulk__pic { display: grid; gap: var(--space-1); align-content: start; min-width: 0; }
.image-bulk__pic canvas { display: block; width: 100%; max-width: 100%; height: auto; background: var(--wash); }
.image-bulk__label { font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); }
.image-bulk__name { grid-column: 1 / -1; font-size: var(--text-small); overflow-wrap: anywhere; }

/* The pictures chosen on an upload form, each with its own Edit. */
.image-queue { list-style: none; margin: var(--space-2) 0 0; padding: 0; display: grid; gap: var(--space-2); }
.image-queue__item { display: flex; gap: var(--space-3); align-items: center; }
.image-queue__thumb { width: 3rem; height: 3rem; object-fit: cover; background: var(--wash); flex: none; }
.image-queue__name { flex: 1; font-size: var(--text-small); overflow-wrap: anywhere; }
.image-queue__caption { flex: 2; min-width: 8rem; }
.image-queue__item { flex-wrap: wrap; }

/* Condition reports (the condition events amendment): the fields, the timeline,
   the photographs, and the same on paper. */
.condition-fields { border: 1px solid var(--line); padding: var(--space-3) var(--space-4); margin: var(--space-4) 0; min-width: 0; }
.condition-fields legend { padding: 0 var(--space-2); font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); }
.condition-pick { position: relative; cursor: pointer; }
.condition-pick input[type=file] { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
.condition-pick:focus-within { outline: 2px solid var(--ink); outline-offset: 2px; }
.condition-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-4); }
.condition-event { border-left: 3px solid var(--line); padding: 0 0 0 var(--space-3); }
.condition-event__head { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; }
.condition-event__text { margin: var(--space-2) 0; }
.condition-photos { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-2) 0; }
.condition-photo { margin: 0; width: 8rem; }
.condition-photo__img { display: block; width: 8rem; height: 8rem; object-fit: cover; background: var(--wash); border: 1px solid var(--line); }
.condition-photo figcaption { margin-top: var(--space-1); color: var(--ink-muted); overflow-wrap: anywhere; }
.condition-resolve { margin-top: var(--space-2); }
.condition-print__event { margin: var(--space-3) 0; break-inside: avoid; }
.condition-print__photos { display: flex; flex-wrap: wrap; gap: 4mm; margin-top: var(--space-2); }
.condition-print__photo { margin: 0; width: 40mm; }
.condition-print__photo img { display: block; width: 40mm; height: 40mm; object-fit: contain; background: #fff; border: 1px solid var(--line); }
.condition-print__photo figcaption { font-size: var(--text-small); color: var(--ink-muted); margin-top: 1mm; overflow-wrap: anywhere; }
@media print { .condition-print__photo img { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }

/* ------------------------------------------------------ Tabs, pagination -- */

.tabs { display: flex; gap: var(--space-6); border-bottom: 1px solid var(--line); margin-bottom: var(--space-6); }
.tabs a {
  text-decoration: none; padding: var(--space-3) 0; margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  font-size: var(--text-small); letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--ink-muted);
}
.tabs a[aria-current] { border-bottom-color: var(--ink); color: var(--ink); }

.pagination { display: flex; gap: var(--space-2); align-items: center; margin-top: var(--space-6); }
.pagination__info { color: var(--ink-muted); font-size: var(--text-small); margin-right: auto; }

/* ------------------------------------------------------------- Utility --- */

.stack > * + * { margin-top: var(--space-4); }
.cluster { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.between { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-2) var(--space-4); }
.grow { flex: 1; }
.nowrap { white-space: nowrap; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}


/* ------------------------------------------------------------- Sign in --- */

.auth { max-width: 26rem; margin: var(--space-12) auto 0; }
.auth__brand { text-align: center; margin-bottom: var(--space-8); }
.auth h1 { text-align: center; margin-bottom: var(--space-6); }
.auth__brand .wordmark__name { font-size: 1.375rem; }
.auth__brand .wordmark__sub { font-size: 0.625rem; }
@media (max-width: 40rem) { .auth { margin-top: var(--space-6); } }

/* ------------------------------------------------------------ Utilities -- */
.flush { margin: 0 !important; }
.is-danger { color: var(--danger); }
.tile-img { width: 100%; height: 8rem; object-fit: cover; background: var(--wash); }

/* The work's picture beside its title on the artwork page. Whole, not
   cropped: a landscape and a portrait both have to read as themselves. */
.artwork-head { display: flex; gap: var(--space-4); align-items: flex-start; margin-bottom: var(--space-3); }
.artwork-head__text { flex: 1; min-width: 0; }
.artwork-head__picture {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 12rem; height: 12rem; background: var(--wash);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.artwork-head__picture img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.artwork-head__picture--none { font-size: var(--text-small); text-decoration: none; }
@media (max-width: 40rem) {
  .artwork-head { flex-direction: column; }
  .artwork-head__picture { width: 100%; height: 14rem; }
}

/* The artist's photo beside their name on the artist's page. Laid out as the
   artwork's is, but a person and not a work: portrait (4:5), filled to the frame
   and cropped, never stretched. The frame has its shape before the picture
   arrives, so the page does not jump. With no photo it holds initials. */
.artist-head { display: flex; gap: var(--space-4); align-items: flex-start; margin-bottom: var(--space-3); }
.artist-head__photo { flex: none; width: 12rem; display: flex; flex-direction: column; gap: var(--space-2); }
.artist-head__text { flex: 1; min-width: 0; }
.artist-head__text .lede { margin-bottom: var(--space-2); }
.artist-head__facts { margin: 0 0 var(--space-3); }
.artist-head__picture {
  width: 100%; aspect-ratio: 4 / 5; max-height: 18rem; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--wash); border: 1px solid var(--line); border-radius: var(--radius);
}
.artist-head__picture img { width: 100%; height: 100%; object-fit: cover; display: block; }
.artist-head__picture--none { color: var(--ink-muted); font-size: 2.5rem; letter-spacing: 0.04em; }
.artist-head__change { font-size: var(--text-small); }
/* The record's actions (Privacy request, Deactivate, Edit) at the top right of the head, as a page header's are (the UI polish pass, 30 October 2026). */
.artist-head__actions { align-self: flex-start; flex: 0 0 auto; }
@media (max-width: 40rem) {
  .artist-head { flex-direction: column; }
  .artist-head__photo { width: 100%; max-width: 14rem; }
  .artist-head__actions { margin-left: 0; }
}

/* An artwork's photographs: a thumbnail that opens the original, and what it is under it. */
.image-tiles { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-4); }
.image-tile { border: 1px solid var(--line); padding: var(--space-2); width: 11rem; }
.image-tile__link { display: block; cursor: zoom-in; }
.image-tile__meta {
  font-size: var(--text-small); margin: var(--space-2) 0; font-variant-numeric: tabular-nums;
  display: grid; gap: var(--space-1);
}
.image-tile__meta .cluster { gap: var(--space-1); }
.narrow { max-width: 8rem; }
.image-tile {
  border: 1px solid var(--line); padding: var(--space-2); width: 11rem;
}

/* The Images panel of a work's own page (15 October 2026). A work has two or three pictures, so each gets a wide row: the picture large on the left, in its
   own shape and never cropped, and beside it, from the top, its badges and order, its size, its description, the wall preview tick and the buttons, with
   the destructive one at the far end. On a phone the picture sits above its details. The page for one print and the stock screens still use .image-tile. */
.image-rows { display: grid; gap: var(--space-4); margin-bottom: var(--space-4); max-width: 72rem; }
.image-rows__help { margin: 0 0 var(--space-3); }
.image-row {
  display: grid; grid-template-columns: minmax(0, 21rem) minmax(0, 1fr); gap: var(--space-6);
  border: 1px solid var(--line); padding: var(--space-4);
}
.image-row__picture {
  align-self: start; padding: var(--space-2);
  background: var(--wash); border: 1px solid var(--line); border-radius: var(--radius);
}
.image-row__picture .image-tile__link { display: block; }
.image-preview { display: block; width: 100%; height: auto; max-height: 26rem; object-fit: contain; }
.image-row__body { min-width: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.image-row__body > .field { margin-bottom: 0; }
.image-row__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); }
.image-row__title { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.image-row__title strong { font-size: var(--text-h3); font-weight: 500; }
.image-row__order {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); font-weight: 500;
}
.image-row__order input { width: 4.5rem; text-align: center; letter-spacing: normal; }
.image-row__facts { margin: 0; font-variant-numeric: tabular-nums; }
.image-row__alt textarea { min-height: 4.5rem; }
.image-row__wall .choice { min-height: 0; }
.image-row__actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-4);
  margin-top: var(--space-1); padding-top: var(--space-3); border-top: 1px solid var(--line);
}
.image-row__buttons { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.image-row__danger { margin-left: auto; padding-left: var(--space-4); border-left: 1px solid var(--line); }
.image-bulk { margin-bottom: var(--space-4); }
.image-bulk__label { margin-right: var(--space-1); }
@media (max-width: 60rem) { .image-row { grid-template-columns: minmax(0, 16rem) minmax(0, 1fr); gap: var(--space-4); } }
@media (max-width: 42rem) {
  .image-row { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
  .image-row__picture { max-width: 24rem; }
  .image-row__danger { margin-left: 0; padding-left: 0; border-left: 0; flex: 1 1 100%; }
}

/* Adding pictures to a work, in the width of the rows above: the upload and the web address side by side. */
.image-add { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: var(--space-4); margin-top: var(--space-4); max-width: 72rem; }
.image-add__block { border: 1px solid var(--line); padding: var(--space-4); min-width: 0; }
.image-add__block h3 { font-size: var(--text-body); margin-bottom: var(--space-2); }
.image-add__block > form.row { flex-direction: column; align-items: flex-start; }
.image-add__block > form.row > .field { width: 100%; flex: 0 0 auto; }
.image-add__block > form.row > .field:has(> .btn:only-child) { width: auto; }

/* The Rooms panel of a work's page (13 October 2026): every room the work suits, the ones the website shows now first. */
.room-tiles { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-3); }
.room-tile { border: 1px solid var(--line); padding: var(--space-2); width: 11rem; }
.room-tile img { display: block; width: 100%; height: auto; }
.room-tile__name { margin: var(--space-2) 0 0; font-weight: 600; min-height: 2lh; }
.room-tile__match { margin: 0 0 var(--space-2); }

/* The wall preview above the room choices: one room, the work hung in it, and a JPEG download of that composition. */
.wall-preview { margin: var(--space-4) 0 var(--space-6); padding-bottom: var(--space-5); border-bottom: 1px solid var(--line); }
.wall-preview h3 { font-size: var(--text-body); margin-bottom: var(--space-2); }
.wall-preview [hidden] { display: none; }
.wall-preview__room { max-width: 22rem; }
.wall-preview__frame { margin: 0 0 var(--space-3); max-width: 26rem; }
.wall-preview__canvas { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; background: var(--wash); border: 1px solid var(--line); }
.wall-preview__frame[data-state='rendering'] .wall-preview__canvas { opacity: 0.55; }
.wall-preview__placeholder { display: flex; align-items: center; justify-content: center; margin: 0; aspect-ratio: 4 / 5; background: var(--wash); border: 1px solid var(--line); text-align: center; padding: var(--space-4); }
.wall-preview__status { min-height: 1lh; margin: 0 0 var(--space-3); }
.wall-preview .notice { margin-bottom: var(--space-3); }
.wall-preview__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* ----------------------------------------------------------------- Print -- */
/* Receipts (stage 5) and artwork labels must print cleanly: no interface
   chrome, no backgrounds eating toner, black on white. */

@media print {
  .app-header, .app-footer, .filters, .page-header__actions,
  .btn, .no-print { display: none !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  .app-main { max-width: none; padding: 0; }
  .panel { border: 0; padding: 0; margin-bottom: 1rem; break-inside: avoid; }
  .table-wrap { overflow: visible; border: 0; }
  thead th { background: transparent; color: #000; border-bottom: 1px solid #000; }
  tbody tr { border-bottom: 1px solid #ccc; }
  .badge { border-color: #000; color: #000; background: transparent; }
  a { text-decoration: none; color: #000; }
  @page { margin: 12mm; }
}

/* --------------------------------------------------- Settings sections -- */

.page-header__back { margin: 0 0 var(--space-2); font-size: var(--text-small); }
.page-header__back a { color: var(--ink-muted); text-decoration: none; }
.page-header__back a:hover { color: var(--ink); text-decoration: underline; }

/* Settings is grouped by who owns the answer, not by how often it changes:
   the work itself, then how this gallery is set up, then Flag's own plumbing.
   A gallery manager should not have to read past eight technical entries to
   find the one page they came for. */
.settings-group { margin-bottom: var(--space-8); }
.settings-group__title {
  font-size: var(--text-title);
  letter-spacing: var(--tracking-title);
  font-weight: 400;
  margin: 0 0 var(--space-1);
  padding-top: var(--space-6);
  border-top: 1px solid var(--line);
}
.settings-group:first-of-type .settings-group__title { padding-top: 0; border-top: 0; }
.settings-group__blurb {
  color: var(--ink-muted);
  font-size: var(--text-small);
  max-width: var(--measure);
  margin: 0 0 var(--space-4);
}
.settings-group .section-grid { margin-bottom: 0; }
/* A smaller heading inside one group of cards (the Reports page's "Stock and
   merchandise", 3 October 2026): a kind of card of its own under the others. */
.settings-group__subtitle {
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-weight: 400;
  color: var(--ink-muted);
  margin: var(--space-6) 0 var(--space-3);
}

/* The star on a Reports card (3 October 2026).

   A form of its own beside the card's icon, above the stretched link
   (z-index 1, as the count on a Gallery Management card is), so the card is
   one box with a link over it and a button on it and no form is ever inside a
   link. The title keeps clear of both glyphs. The button is a touch target
   larger than the star in it. */
.section-card--icon.section-card--report .section-card__label { padding-right: 5.5rem; }
.section-card__kind { display: block; margin-top: var(--space-2); }
.report-star {
  position: absolute; z-index: 1; margin: 0;
  top: calc(var(--space-4) - 0.25rem); right: calc(var(--space-5) + 2rem);
}
.report-star__button {
  display: flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; padding: 0;
  border: 0; background: none; cursor: pointer;
  color: var(--ink-muted);
}
.report-star__button svg { width: 1.5rem; height: 1.5rem; }
.report-star__button:hover,
.report-star__button[aria-pressed="true"] { color: var(--ink); }
.report-star__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 0; }
/* Gallery Management's cards carry the same star (8 October 2026) and also the
   count of what is waiting, which shares the title's row. The title keeps clear
   of the star and the icon as on a report, and the count drops under the title
   when the two do not fit side by side in what is left. */
.section-card--starred .section-card__label { flex-wrap: wrap; row-gap: var(--space-1); }

/* A section index: each card is one page, named and explained, because a bare
   list of nine links tells a new manager nothing about which one they want. */
.section-grid {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  margin-bottom: var(--space-8);
}
.section-card {
  display: block; text-decoration: none; color: inherit;
  border: 1px solid var(--line);
  background: var(--paper);
  padding: var(--space-4) var(--space-6) var(--space-6);
}
.section-card:hover { background: var(--wash); }
.section-card:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.section-card__label {
  display: block; margin-bottom: var(--space-2);
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.section-card__blurb {
  display: block;
  color: var(--ink-muted);
  line-height: var(--leading-body);
}
.section-card__label {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3);
}

/* A section's icon (28 September 2026): a line drawing in the ink, on nothing,
   in the tile's top right corner with the name top left (David: the icon on a
   grey square stood out too much, and above the name it pushed the name down).
   Pinned rather than put in the name's row, because on Gallery Management that
   row already ends in the count of what is waiting; the row keeps clear of the
   corner instead, so the count stops short of the icon. The card's own link
   is stretched over the whole card, so the icon is part of what opens it. */
.section-card--icon { position: relative; }
.section-card--icon .section-card__label { padding-right: 2.75rem; min-height: 1.75rem; align-items: center; }
.section-card__icon {
  position: absolute; top: var(--space-4); right: var(--space-5);
  display: flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem;
  color: var(--ink);
}
.section-card__icon svg { width: 1.75rem; height: 1.75rem; }

/* What is waiting in a section, on its card.

   Deliberately quiet — it is a number somebody should act on, not an alarm,
   and a page of ten red badges is a page nobody reads. Only sections with
   something waiting carry one at all: a row of zeroes teaches people to stop
   looking at the row. */
/* A card whose count is a link of its own (Gallery Management, 27 September
   2026): the count opens the tasks it counted, the rest of the card opens the
   section. Two links cannot nest, so the card is a box, its name's link is
   stretched over the whole of it, and the count sits above that. */
.section-card--split { position: relative; }
.section-card--split:focus-within { outline: 2px solid var(--ink); outline-offset: 2px; }
.section-card__link { color: inherit; text-decoration: none; }
.section-card__link:focus-visible { outline: none; }
.section-card__link::after { content: ''; position: absolute; inset: 0; }
a.section-card__count { position: relative; z-index: 1; text-decoration: none; }
a.section-card__count:hover { background: var(--paper); color: var(--ink); }
.section-card__count {
  flex: none;
  font-size: var(--text-small);
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-muted);
  background: var(--wash);
  border: 1px solid var(--line);
  padding: 0 var(--space-2);
  white-space: nowrap;
}

/* A tag on a work, and the button that takes it off.

   One control rather than a label with a cross beside it: the whole chip is
   the remove button, because a four-pixel target beside a word is a target
   somebody misses. It looks like a label until it is hovered, which is the
   right emphasis -- reading the tags is the common act and removing one is
   not. */
.chip {
  display: inline-flex; align-items: baseline; gap: var(--space-2);
  font: inherit; font-size: var(--text-small);
  color: var(--ink-muted);
  background: var(--wash);
  border: 1px solid var(--line);
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
}
.chip:hover { color: var(--ink); border-color: var(--ink); background: var(--paper); }
.chip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ------------------------------------------- Section sub-navigation ----- */

/* Gallery Management's own list of sections: one control at every width (19 October 2026, David: "do the same for the big screen").
   A closed bar that names the section you are in, with a chevron, and the sections under it when it is opened. One piece of markup with no script: a `<details>`, which every
   browser closes natively, so there is no rule here forcing it open or shut (the old desktop row relied on one, and a current Chrome stopped honouring it, which hid the row). */
.sub-nav {
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-3);
}
.sub-nav__summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  cursor: pointer; list-style: none;
  width: 100%; max-width: 22rem;
  min-height: 2.75rem; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--control-border); background: var(--wash);
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.sub-nav__summary::-webkit-details-marker { display: none; }
.sub-nav__summary::marker { content: ''; }
.sub-nav__prefix { color: var(--ink-muted); }
.sub-nav__current { flex: 1; text-align: right; }
.sub-nav__summary::after {
  content: ''; flex: none; width: 0.55em; height: 0.55em;
  border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink);
  transform: translateY(-0.15em) rotate(45deg);
}
.sub-nav[open] > .sub-nav__summary::after { transform: translateY(0.1em) rotate(-135deg); }
.sub-nav__summary:hover { background: var(--paper); }
.sub-nav__summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.sub-nav__links {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 0 var(--space-5);
  list-style: none; margin: 0; padding: var(--space-3) 0 var(--space-2);
}
.sub-nav__links a {
  display: block; padding: var(--space-2) 0;
  color: var(--ink-muted); text-decoration: none;
  font-size: var(--text-small);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.sub-nav__links a:hover { color: var(--ink); }
.sub-nav__links a[aria-current="page"] {
  color: var(--ink);
  box-shadow: inset 0 -2px 0 var(--ink);
}
.sub-nav__links a:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (max-width: 40rem) {
  .sub-nav__summary { max-width: none; }
  .sub-nav__links { grid-template-columns: 1fr; }
}

.btn--small { min-height: 2rem; padding: 0 var(--space-3); font-size: var(--text-small); }

/* ------------------------------------------------- Artist image source -- */

.image-source { border-top: 1px solid var(--line); margin-top: var(--space-6); padding-top: var(--space-6); }
.image-source h3 { font-size: var(--text-body); margin-bottom: var(--space-2); }
.image-source__choice { display: flex; gap: var(--space-6); margin: var(--space-4) 0; flex-wrap: wrap; }
.image-source__choice label { display: inline-flex; align-items: center; gap: var(--space-2); }
.image-source__preview { margin-bottom: var(--space-4); color: var(--ink-muted); font-size: var(--text-small); }
.image-source__preview img { max-width: 12rem; border: 1px solid var(--line); display: block; }
/* The area is the landing place after an upload (`#profile-image`, tabindex -1): it takes focus without a ring round the whole block. */
.image-source:focus { outline: none; }
/* "Upload this photo" and, beside it, the note that says nothing has been sent yet (public/form-guard.js fills it once a file is chosen). */
.row > .field.image-source__send:has(> .btn) { flex: 1 1 18rem; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
.image-source__unsent { font-size: var(--text-small); font-weight: 600; color: var(--warn-deep, var(--warn)); }
.image-source__unsent:empty { display: none; }
.block { display: block; }
.is-warn { color: var(--warn); }
.preview-thumb {
  width: 3rem; height: 3rem; object-fit: cover;
  border: 1px solid var(--line); background: var(--wash); display: block;
}
.image-source__current {
  display: flex; gap: var(--space-6); align-items: flex-start;
  margin-bottom: var(--space-6); flex-wrap: wrap;
}
.image-source__current .tile-img { max-width: 11rem; }

/* ------------------------------------------------------ Accounting checks -- */

/* A sale whose figures do not add up has to be impossible to skim past. */
.panel--flagged { border-left: 3px solid var(--danger); }

/* Where a form comes back to: clear of the sticky header, so the answer above
   the panels is in view rather than under it. */
.anchor-target { scroll-margin-top: 5rem; }

/*
 * The title and the actions that are about the record rather than about a
 * customer. Wraps to two lines on a phone rather than squeezing the title.
 */
.page-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--space-3);
}
.page-head > h1 { margin-bottom: 0; }
.page-head > .cluster { margin-bottom: 0; }

/*
 * The buttons beside the work's title (15 October 2026): Edit, Labels and Ask for more like this, small; Hold and Artist request; then Sell,
 * the largest, at the right end with the choice of print beside it when the work is a print with several for sale (the primary is rightmost
 * in every page header since the UI polish pass of 30 October 2026, as it is in every form). A Sell or Hold that cannot be used is drawn
 * disabled, and one sentence under the row says why. On a phone Sell comes first and takes the whole row.
 */
.artwork-actions { gap: var(--space-3); justify-content: flex-end; }
.artwork-actions__sell { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 0; }
.artwork-actions__sell select { width: auto; max-width: 16rem; min-height: 3.25rem; }
.artwork-actions__label { font-size: var(--text-small); color: var(--ink-muted); }
@media (max-width: 40rem) {
  .artwork-actions { justify-content: flex-start; }
  .artwork-actions__sell { flex: 1 1 100%; order: -1; }
  .artwork-actions__sell .btn { flex: 1 1 8rem; }
  .artwork-actions__sell select { flex: 1 1 10rem; max-width: none; }
}

/* Who has the work held, and what has been asked for like it: two short lines under the availability. */
.head-facts {
  display: grid; grid-template-columns: max-content 1fr; gap: var(--space-1) var(--space-4);
  margin: var(--space-3) 0 0; font-size: var(--text-small);
}
.head-facts > dt { color: var(--ink-muted); }
.head-facts > dd { margin: 0; min-width: 0; }

/* The work at the top of its Hold screen: a small picture and the lines that say which work it is. */
.hold-work { display: flex; gap: var(--space-4); align-items: center; margin-bottom: var(--space-6); }
.hold-work__picture {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 7rem; height: 7rem; background: var(--wash);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.hold-work__picture img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.hold-work__text { min-width: 0; }
.hold-work__text h2 { margin-bottom: var(--space-1); }
.hold-work__text .lede, .hold-work__text p { margin-bottom: var(--space-2); }
tr.is-flagged td { background: color-mix(in srgb, var(--danger) 6%, transparent); }

.checks { list-style: none; margin: 0 0 var(--space-6); padding: 0; }
.stack { display: grid; gap: var(--space-1); } /* a column of tick boxes, one under another */
.stack > label.check { display: flex; align-items: center; gap: var(--space-2); }
/* A list of tick boxes with a sentence each (the customer's lists): one under another, the box beside the first line of its words. */
.checks > label.check { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-1) 0; }
.checks > label.check > input { margin-top: 0.25rem; flex: none; }
.checks__item { display: flex; gap: var(--space-3); padding: var(--space-2) 0; align-items: baseline; }
.checks__mark { font-weight: 600; }
.checks__item.is-ok .checks__mark { color: var(--ink-muted); }
.checks__item.is-bad .checks__mark { color: var(--danger); }
.checks__item.is-bad strong { color: var(--danger); }
/*
 * The Month end checklist (31 October 2026): a mark, the words and the one link. On a phone the mark is beside the words and the link sits under them as a small button; on a wide screen the link
 * is at the right of the same row, so six steps are six short rows and not six paragraphs.
 */
.checklist .checks__item { display: grid; grid-template-columns: 1.25rem minmax(0, 1fr); column-gap: var(--space-3); align-items: start; border-bottom: 1px solid var(--line); }
.checklist .checks__item:last-child { border-bottom: 0; }
.checklist__words { min-width: 0; }
.checklist__link { grid-column: 2; margin-top: var(--space-1); }
@media (min-width: 48rem) {
  .checklist .checks__item { grid-template-columns: 1.25rem minmax(0, 1fr) auto; align-items: center; }
  .checklist__link { grid-column: auto; margin-top: 0; }
}

/* ------------------------------------------------------------ Stock take -- */

/* A counted row should read as done at arm's length, on a phone, in gallery
   light — so it is a left edge and a wash, not a tick to squint at. */
tr.is-counted td { background: color-mix(in srgb, var(--sage) 12%, transparent); }
tr.is-counted td:first-child { box-shadow: inset 3px 0 0 var(--sage); }

.resolve { display: grid; gap: var(--space-2); }
.resolve .field { margin: 0; }
.resolve > .btn { justify-self: end; } /* the form's action at the right end of its cell */

/* ---------------------------------------------------------------- Reports -- */

/* A printed report has to say what it is. On screen that line is noise,
   because the parameters are visible in the form directly above it. */
.print-only { display: none; }
@media print {
  .print-only { display: block; }
  .filters { display: none; }
}

/* ------------------------------------------------------- Receipt --- */

/*
 * The actions above a receipt.
 *
 * Stacked rows rather than one wrapping cluster. They were a single row
 * holding a button, a form with its own input and button, a link and a line
 * of help — four things of three heights, wrapping against each other into
 * what read as boxes scattered about.
 */
.receipt-actions {
  display: grid; gap: var(--space-4);
  padding-bottom: var(--space-5); margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--line);
}
.receipt-actions .row { align-items: start; }
.receipt-actions .field { margin-bottom: 0; }

/* ------------------------------------------ Printed gift voucher --- */

/*
 * The voucher somebody is handed.
 *
 * Centred, generous, and built so the **code is the largest thing on it** —
 * because at the other end of this voucher's life a person is reading it off
 * paper and typing it into a box. Everything else on the card is context for
 * that one string.
 *
 * Sized in millimetres rather than rem: this is a thing that comes out of a
 * printer at a known size, half an A4 sheet, and not a page that reflows.
 */
.voucher-print {
  max-width: 150mm;
  margin: 0 auto;
  padding: var(--space-8) var(--space-6);
  border: 2px solid var(--ink);
  text-align: center;
  break-inside: avoid;
}
.voucher-print p { margin: 0; }
.voucher-print__gallery {
  font-size: var(--text-small); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-muted);
}
.voucher-print__title {
  margin-top: var(--space-2) !important;
  font-size: var(--text-h2); font-weight: 600;
}
.voucher-print__value {
  margin-top: var(--space-5) !important;
  font-size: 2.75rem; font-weight: 600; line-height: 1.1;
}
.voucher-print__code {
  margin-top: var(--space-4) !important;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  display: inline-block;
  font-size: 1.5rem; letter-spacing: 0.14em;
}
.voucher-print__for {
  margin-top: var(--space-5) !important; font-size: var(--text-h3);
}
.voucher-print__message {
  margin-top: var(--space-3) !important;
  font-style: italic; color: var(--ink-muted);
}
/* When it can be used: main text, not small print (section 36WC). */
.voucher-print__valid { margin-top: var(--space-5); }
.voucher-print__valid-line { font-size: var(--text-h3); font-weight: 600; line-height: 1.4; }
.voucher-print__valid-note { margin-top: var(--space-2) !important; font-size: var(--text-small); color: var(--ink); }
.voucher-print__facts {
  width: auto; margin: var(--space-6) auto 0;
  font-size: var(--text-small);
}
.voucher-print__facts th,
.voucher-print__facts td {
  border: 0; padding: 0.15rem var(--space-3); background: transparent;
}
.voucher-print__facts th { text-align: right; color: var(--ink-muted); font-weight: 400; }
.voucher-print__facts td { text-align: left; }
/* Brackets and words, never colour alone. */
.voucher-print__void {
  margin-top: var(--space-5) !important;
  padding: var(--space-3); border: 1px solid var(--ink);
  font-weight: 600;
}
.voucher-print__terms,
.voucher-print__gallery-details {
  margin-top: var(--space-5) !important;
  font-size: var(--text-small); color: var(--ink-muted);
}
/* The opening hours are one line per day range, as they are in the website
   footer and the portal, and they are stored with the line breaks in them. */
.voucher-print__gallery-details { white-space: pre-line; }

@media print {
  /* The border is the voucher, so it survives the print reset that strips
     every other border in the page. */
  .voucher-print { border: 2px solid #000; padding: 10mm; }
  .voucher-print__code { border: 1px solid #000; }
  .voucher-print__void { border: 1px solid #000; }
}

/* -------------------------------------------------- Detail pairs --- */

/*
 * A record's facts as label and value, for a page that reads rather than
 * edits: what an invoice said, what a stock item is.
 *
 * Two columns while there is room for them, one on a phone — a label sitting
 * above its value is fine, a label squeezed to two words a line is not.
 */
.detail-grid {
  display: grid; grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-5); margin: 0;
}
.detail-grid dt { color: var(--ink-muted); font-size: var(--text-small); }
.detail-grid dd { margin: 0; }

@media (max-width: 30rem) {
  .detail-grid { grid-template-columns: 1fr; gap: var(--space-1); }
  .detail-grid dd { margin-bottom: var(--space-3); }
}

/* A list that is a list of facts, not of choices: no bullets, no indent. */
.plain { list-style: none; margin: 0; padding: 0; }
.plain > li + li { margin-top: var(--space-2); }

/* A block of text meant to be copied out whole: an order request, a note to
   send somebody. Wraps rather than scrolls — this is prose, not code. */
.preformatted {
  white-space: pre-wrap; font-family: inherit; font-size: var(--text-small);
  background: var(--surface-sunken, var(--paper)); border: 1px solid var(--line);
  padding: var(--space-4); margin: var(--space-3) 0; border-radius: 0;
  overflow-wrap: anywhere;
}

/*
 * Money that went the other way: a refund, a credit, a reversal.
 *
 * Brackets in the markup and colour here, never colour alone — a figure whose
 * only sign is that it is red is unreadable in print, in greyscale, and to
 * anyone who cannot separate the hues. The same rule the status badges follow.
 */
.is-negative { color: var(--danger); font-variant-numeric: tabular-nums; }

/*
 * Preformatted blocks — a courier's JSON, a Xero payload, a stack trace.
 *
 * There was no rule at all, so a single long line pushed the whole page
 * sideways. Scrolling rather than wrapping, because wrapping JSON destroys
 * the indentation that makes it readable, and this is read to debug with.
 */
pre, pre.mono {
  max-width: 100%;
  overflow-x: auto;
  /*
   * `.mono` sets `white-space: nowrap`, which is right for an inline SKU or a
   * quote id and ruinous for a block of JSON: it folds nineteen indented
   * lines onto one and stretches the block to 2,800px. Every request and
   * response on the shipping diagnostics page was rendering that way.
   */
  white-space: pre;
  margin: 0 0 var(--space-3);
  padding: var(--space-3);
  background: var(--wash);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/*
 * A row that holds the detail for the row above it.
 *
 * Given no top border so it reads as part of its own offer rather than as a
 * separate entry, and its summary is offset to sit under the carrier name.
 */
.detail-row > td { border-top: 0; padding-top: 0; }
.detail-row summary {
  cursor: pointer;
  color: var(--ink-muted);
  font-size: var(--text-small);
  padding: var(--space-1) 0;
}
.detail-row summary:hover { color: var(--ink); }
.detail-row details[open] summary { margin-bottom: var(--space-3); }

/* ------------------------------------------------------ Address lookup -- */
/* The search box above an address form. Built by /address-lookup.js, so none
   of this is on the page unless the script ran — which is the point: a
   control that cannot work should not be visible. */

.address-lookup { position: relative; }

/* The list overlays what follows rather than pushing the form down as
   somebody types, which would move the fields they are about to read. */
.address-lookup__list {
  position: absolute; z-index: 20; left: 0; right: 0;
  margin: 0; padding: var(--space-1) 0; list-style: none;
  max-height: 16rem; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgb(0 0 0 / 0.12);
}

.address-lookup__option {
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  font-size: var(--text-body);
}

/* Hover and keyboard selection look the same, because they mean the same
   thing: this is the one you are about to choose. */
.address-lookup__option:hover,
.address-lookup__option.is-active {
  background: var(--ink); color: var(--paper);
}

/* Announced to a screen reader and shown to everybody else: the same words
   either way, rather than a visual state a reader cannot reach. */
.address-lookup__status {
  font-size: var(--text-small); color: var(--ink-muted);
  margin-top: var(--space-1); min-height: 1.25em;
}

/* ------------------------------------------------ Consignment documents -- */

/*
 * The consignment note and the release (stage 11D item 6). HTML with a print
 * stylesheet rather than a generated PDF — the call receipts made at stage 5,
 * remittances at 6C and contracts at 11. Both are handed to an artist and both
 * are signed, so they are laid out for paper first and read as a document on
 * screen because that is what they are.
 */
.document { max-width: 46rem; margin: 0 auto; padding: var(--space-6) var(--space-4); }
@media screen and (max-width: 48rem) { .document { overflow-x: auto; } } /* a figures table wider than a phone scrolls inside the sheet, and the page does not slide */
.document header { border-bottom: 1px solid var(--rule); padding-bottom: var(--space-4); }
.document h1 { margin: 0; }
.document h2 {
  font-size: var(--text-small);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  margin: var(--space-6) 0 var(--space-2);
}
.document__ref { margin: var(--space-1) 0 0; color: var(--ink-muted); }
.document table { width: 100%; margin-top: var(--space-2); }
.document tfoot th { border-top: 1px solid var(--ink); }

/*
 * Room to sign, on the copy that has not been accepted yet. Two rules rather
 * than a bordered box, because this is printed and then written on.
 */
.signature-line {
  display: flex;
  gap: var(--space-6);
  margin-top: var(--space-6);
}
.signature-line span {
  flex: 1;
  border-top: 1px solid var(--ink);
  padding-top: var(--space-2);
  font-size: var(--text-small);
  color: var(--ink-muted);
}
.signature-line span:last-child { flex: 0 0 12rem; }

/*
 * The print, as written on it: 3/50, A/P, Test 1.
 *
 * Tabular numerals so a column of them lines up, and never colour alone —
 * this is an identity, and on the consignment note it is what the artist is
 * signing for.
 */
.edition-label {
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}

.document .edition-label { color: inherit; font-weight: 600; }

/* ------------------------------------------------------------------ */
/* Tasks and messages (stage 14)                                       */
/* ------------------------------------------------------------------ */

/*
 * A list of things to do, which is a list before it is anything else.
 *
 * A real <ul>, so a screen reader says how many there are and a keyboard user
 * can move through them, and so the dashboard panel and the Gallery Management
 * list are the same markup.
 */
.task-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

/*
 * A row is a grid, and every row has the same columns (15 October 2026).
 *
 * It was a flex line spaced between its children. A row with a tick box had three of them (the box, the words, the buttons), so
 * the words floated to the middle; a row with none had two, and the words sat on the left. Now the words take the column that
 * is left over and the buttons the width they need, and a list that offers tick boxes (`task--selectable`) draws a first
 * column of the same width on every row, empty where a task has no box, so nothing moves.
 */
.task {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2) var(--space-3);
  align-items: start;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}
.task--selectable { grid-template-columns: 1.375rem minmax(0, 1fr) auto; }

.task__body { min-width: 0; }

.task__title {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
  line-height: var(--leading-tight);
}

/*
 * The facts under the title, separated by middots.
 *
 * The separators are in the markup rather than in ::before, because a row may
 * legitimately have no due date and no link, and a CSS separator would leave a
 * middot hanging at the front of the line. They carry aria-hidden, so the line
 * is not read out as "from Flag dot anyone dot overdue".
 */
.task__meta {
  margin: var(--space-1) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
}

/*
 * What a task is about, under its words, in one style whatever it is about (15 October 2026): a small label for the kind of thing, then the
 * record's own card. For a work that is the picture with the title over "artist, FG code" (`.artwork-line`); for anything else, and for the
 * facts behind a task Flag raised, a name over one fact, set out the same way so the rows line up whatever they hold. Nothing is drawn for a
 * task with nothing to say, so there is never an empty box.
 */
.task__data {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-width: 40rem;
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--wash);
  border: 1px solid var(--line);
  min-width: 0;
}
.task__data-label {
  flex: 0 0 5.5rem;
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  line-height: var(--leading-tight);
}
.task__data-text { display: grid; min-width: 0; }
.task__data-name { font-weight: 500; }
.task__data-fact { font-size: var(--text-small); color: var(--ink-muted); overflow-wrap: anywhere; }
@media (max-width: 40rem) {
  .task__data { flex-direction: column; align-items: flex-start; gap: var(--space-1); }
  .task__data-label { flex: none; }
}

.task__actions { justify-content: flex-end; }
.task__actions form { margin: 0; }

/*
 * Overdue and high priority, marked on the left edge.
 *
 * A border rather than a background, so the row stays as readable as the ones
 * around it, and never colour alone — the row also carries the word "Overdue"
 * and a "High" badge. Both survive greyscale, which is how these are printed
 * for the Monday meeting.
 */
.task--overdue, .task--high { position: relative; }
.task--overdue::before, .task--high:not(.task--overdue)::before {
  content: ''; position: absolute; left: -0.5rem; top: 0; bottom: 0; width: 3px;
}
.task--overdue::before { background: var(--danger); }
.task--high:not(.task--overdue)::before { background: var(--warn); }

.task-group + .task-group { margin-top: var(--space-5); }

/*
 * The dashboard card's counts and the quick filters, here and on the Tasks page.
 *
 * A chip is a link that turns a filter on, and the one that is on says so with
 * its border and `aria-current`, never colour alone. The counts above them are
 * links too, each opening the list it counted.
 */
.task-counts { margin: 0 0 var(--space-3); font-size: var(--text-small); color: var(--ink-muted); }
.task-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-4); }
.task-chip {
  display: inline-block;
  font-size: var(--text-small);
  color: var(--ink-muted);
  background: var(--wash);
  border: 1px solid var(--line);
  padding: var(--space-1) var(--space-3);
  text-decoration: none;
}
.task-chip:hover { color: var(--ink); border-color: var(--ink); background: var(--paper); }
.task-chip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.task-chip[aria-current] { color: var(--ink); border-color: var(--ink); background: var(--paper); font-weight: 600; }

/* A kind of task shown as one row that opens on a few of them. */
.task--cluster { display: block; }
.task--cluster > details > summary {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: baseline; cursor: pointer;
}
.task--cluster .task-list { margin-top: var(--space-3); }

/* The tick box's cell on a task row, for the bulk bar: as wide with no box in it as with one, and on the line of the title. */
.task__select { display: flex; justify-content: center; }
.task__select input[type='checkbox'] { margin: 0; }

/*
 * The bulk bar above the list (15 October 2026): one short toolbar and no wasted height. A head line (the select-all box, a count of what is
 * ticked, a line of help), then the three actions as labelled groups in one wrapping row (Close, Give to, Snooze until), each a control or two
 * under its own small heading, and the long explanation folded into "How this works". Close is the primary button, the other two are
 * secondary. The date and time of a custom snooze sit in the Snooze group, side by side at the width of a date and a time, and are shown only
 * while "Pick a date and time" is chosen; a browser without :has() shows them always.
 */
.bulk-bar {
  display: grid;
  gap: var(--space-3);
  border: 1px solid var(--line);
  background: var(--wash);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}
.bulk-bar [hidden] { display: none; }
.bulk-bar__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-5); }
.bulk-bar__all { display: inline-flex; align-items: center; margin: 0; font-weight: 500; }
.bulk-bar__count { font-variant-numeric: tabular-nums; font-weight: 500; }
.bulk-bar__line { margin-left: auto; }
.bulk-bar__groups { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3) var(--space-6); }
.bulk-bar__group { display: grid; gap: var(--space-1); }
.bulk-bar__title {
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 500;
}
.bulk-bar__only { text-transform: none; letter-spacing: normal; font-weight: 400; }
.bulk-bar__only::before { content: '· '; }
.bulk-bar__controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.bulk-bar__controls select { width: auto; min-width: 11rem; }
.bulk-bar__when { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.bulk-bar__when input { width: auto; }
.bulk-bar__when input[type='date'] { flex: 0 1 10.5rem; }
.bulk-bar__when input[type='time'] { flex: 0 1 8rem; }
@supports selector(:has(*)) {
  .bulk-bar__when { display: none; }
  .bulk-bar__snooze:has(option[value='date']:checked) .bulk-bar__when { display: flex; }
}
.bulk-bar__how summary { cursor: pointer; font-size: var(--text-small); color: var(--ink-muted); }
.bulk-bar__how ul { margin: var(--space-2) 0 0; padding-left: var(--space-5); display: grid; gap: var(--space-1); }
@media (max-width: 40rem) {
  .bulk-bar__line { margin-left: 0; flex-basis: 100%; }
  .bulk-bar__groups { flex-direction: column; align-items: stretch; }
  .bulk-bar__controls select { flex: 1 1 8rem; min-width: 0; }
  .bulk-bar__group:first-child .btn { flex: 1 1 100%; }
  .bulk-bar__when input[type='date'], .bulk-bar__when input[type='time'] { flex: 1 1 8rem; }
}


.task-group__title {
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0 0 var(--space-2);
}

/* On a phone the actions go under the words rather than squeezing the title into a column two words wide, and the tick box
 * keeps its column, so the words still line up whether or not a row has one. */
@media (max-width: 40rem) {
  .task { grid-template-columns: minmax(0, 1fr); }
  .task--selectable { grid-template-columns: 1.375rem minmax(0, 1fr); }
  .task__actions { grid-column: 1 / -1; justify-content: flex-start; }
  .task--selectable .task__actions { grid-column: 2; }
}

/*
 * The board: the same rows, in columns by status.
 *
 * Columns rather than a grid of cards, because a task is a line of text and a
 * card would make it a line of text in a box. Below a tablet the columns stack,
 * which is the same list in a different order and reads perfectly well.
 */
.task-board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);
  align-items: start;
}

/* A column is narrow, so the row stacks inside it rather than squeezing the
 * title into two words. */
.task-board .task { grid-template-columns: minmax(0, 1fr); }
.task-board .task__actions { grid-column: 1; flex-wrap: wrap; justify-content: flex-start; }

@media (max-width: 60rem) {
  .task-board { grid-template-columns: 1fr; }
}

/* The thread, and the timeline under it. Both are ordered lists, because both
 * are things that happened in an order. */
.task-thread {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
}

.task-thread__item {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}

.task-thread__item:last-child { border-bottom: 0; }
.task-thread__item p:first-child { margin-top: 0; }

/* Said once, where a filtered list has nothing in it. */
.empty {
  padding: var(--space-6) 0;
  text-align: center;
}

/*
 * How much is waiting for you, in the header.
 *
 * A link, because a number you cannot press is decoration. It carries its own
 * words for a screen reader -- "7" on its own is not a sentence -- and it is
 * not rendered at all when there is nothing, so it never says nought.
 */
.task-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding: 0 0.4rem;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-size: var(--text-small);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

.task-badge:hover, .task-badge:focus-visible { background: var(--sage); }

/* ------------------------------------------------ Formatted text (Markdown) -- */
/* Markdown rendered by shared/markdown.ts inside the admin application: an
   artwork's description, a content page's preview. Paragraphs spaced, nothing
   else restyled, so it reads like the rest of the page. */
.prose > * { margin: 0; }
.prose > * + * { margin-top: var(--space-3); }
.prose ul, .prose ol { padding-left: var(--space-6); }
.prose blockquote { border-left: 3px solid var(--line); padding-left: var(--space-3); color: var(--ink-muted); }

/* What Shopify said, kept under the description for checking a mis-read. */
.source-original { margin-top: var(--space-4); font-size: var(--text-small); }
.source-original summary { cursor: pointer; color: var(--ink-muted); }
.source-original .detail-grid { margin: var(--space-3) 0; }
.source-original__html {
  white-space: pre-wrap; word-break: break-word; max-height: 20rem; overflow: auto;
  background: var(--wash); padding: var(--space-3); margin: 0;
}

/* The re-parse preview: what a description reads now, beside what it would read. */
.reparse-sample + .reparse-sample { border-top: 1px solid var(--line); margin-top: var(--space-4); padding-top: var(--space-4); }
.reparse-sample__pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
@media (max-width: 640px) { .reparse-sample__pair { grid-template-columns: 1fr; } }
#preview form { margin-top: var(--space-5); }

/* The Markdown toolbar. Built hidden and shown by its script; `[hidden]` is
   stated here because a class that sets `display` would otherwise beat the
   browser's own rule and show buttons that do nothing. */
.md-toolbar { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-bottom: var(--space-1); }
.md-toolbar[hidden] { display: none; }
/* On a phone or a touch screen its buttons (B, I, a link) are boxes 44 pixels each way, with a gap a thumb can tell apart. */
@media (max-width: 48rem), (pointer: coarse) { .md-toolbar { gap: var(--space-2); } .md-toolbar .btn { min-width: var(--control-height); } }

/* A list's pages: where this one sits, and the way to the next. */
.pager { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-4); }

/* Find as you type (public/find.js): the matches under a search box. */
.find-host { position: relative; }
.find-list {
  position: absolute; z-index: 20; left: 0; right: 0; top: 100%;
  margin: 2px 0 0; padding: 0; list-style: none; max-height: 22rem; overflow-y: auto;
  background: var(--paper); border: 1px solid var(--ink); box-shadow: var(--shadow-modal);
}
.find-list li { padding: var(--space-2) var(--space-3); cursor: pointer; display: grid; gap: 2px; }
.find-list li[aria-selected="true"], .find-list li[role="option"]:hover { background: var(--wash); }
.find-list__note { color: var(--ink-muted); cursor: default; }

/* A match as a picker draws it: the picture, the title over what tells two
   apart with the code in brackets at the end, then the status. The same row
   is the picker's chosen value. Merchandise is set off by its left rule and
   its badge, so a card is never mistaken for a work. */
.find-list li.find-row { display: flex; align-items: center; gap: var(--space-3); }
.find-row { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.find-row__thumb { flex: none; width: 2.5rem; height: 2.5rem; object-fit: cover; background: var(--wash); border: 1px solid var(--line); display: inline-block; }
.find-row__text { display: grid; min-width: 0; flex: 1; }
.find-row__label { color: var(--ink); overflow-wrap: anywhere; } /* a title wraps; it was cut at the edge of a column 32px wide on a phone */
.find-row__detail { font-size: var(--text-small); color: var(--ink-muted); }
.find-row__ref { font-family: var(--font-mono, monospace); }
.find-row__why { font-size: var(--text-small); color: var(--ink-muted); font-style: italic; }
.find-row .badge { flex: none; }
.find-row--merch { box-shadow: inset 3px 0 0 var(--line); }
.badge--merch { color: var(--ink-muted); border-style: dotted; }
/* A service (stage 16): a dashed border and a bar of its own, so it is never mistaken for a work or a card. */
.find-row--service { box-shadow: inset 3px 0 0 var(--line); }
.badge--service { color: var(--ink-muted); border-style: dashed; }
/* A ticket to an event (stage 16, slice 3b): a double border and a bar of its own, so it is never mistaken for a work, a card or a service. */
.find-row--event { box-shadow: inset 3px 0 0 var(--ink-muted); }
.badge--event { color: var(--ink-muted); border-style: double; border-width: 3px; }
.ticket-count { font-variant-numeric: tabular-nums; }
.attendees-print { max-width: 60rem; margin: 0 auto; }
.attendees-print table { width: 100%; border-collapse: collapse; }
.attendees-print th, .attendees-print td { border-bottom: 1px solid var(--line); padding: var(--space-2) var(--space-3); text-align: left; vertical-align: top; }
.attendees-print .tick { width: 2.5rem; }
@media print { .attendees-print { max-width: none; } .no-print { display: none !important; } }
/* Events (stage 16, slice 3a): the colour chip, the bar that says how many places are taken, and the picture at the head of an event. */
.swatch { display: inline-block; width: 0.7rem; height: 0.7rem; border-radius: 50%; border: 1px solid var(--line); vertical-align: -0.05rem; }
.event-type { white-space: nowrap; color: var(--ink-muted); }
.capacity { display: flex; flex-direction: column; gap: var(--space-1); min-width: 7rem; }
.capacity__track { height: 0.4rem; background: var(--wash); border: 1px solid var(--line); }
.capacity__fill { height: 100%; background: var(--ink); }
.event-hero { display: block; width: 100%; max-height: 22rem; object-fit: cover; background: var(--wash); margin-bottom: var(--space-4); }
.event-summary { display: flex; flex-wrap: wrap; gap: var(--space-6); margin-bottom: var(--space-6); }
.event-summary > div { min-width: 8rem; }
.image-tile--wide { width: 100%; max-width: 28rem; }
/* An event's picture on its Details tab is drawn at the shape the website draws it, so it can be judged as the website will show it: 4:3, `EVENT_PICTURE_SHAPE` in src/lib/event-images.ts (held to it by test/event-picture-shape.test.ts). */
.image-tile--event { max-width: 22rem; }
.tile-img--shape { height: auto; aspect-ratio: 4 / 3; }
/* What can be done to a booked service on the sale page (stage 16, slice 2). */
.service-actions { margin-top: var(--space-2); }
.service-actions details { margin-top: var(--space-2); }
.service-actions summary { cursor: pointer; }
.find-row--create { color: var(--ink); font-weight: 600; border-top: 1px solid var(--line); }

/* The Sell search (Batch 10, item 18). Merchandise with none in stock is greyed
   and says why in its detail line, so colour is never the only signal. A quantity
   box sits at the end of a merchandise row, with "Sell anyway" beside it for an
   administrator. */
.find-row--unavailable { opacity: 0.6; }
.find-list li.find-row--unavailable { cursor: not-allowed; }
.find-row__controls { display: inline-flex; align-items: center; gap: var(--space-2); flex: none; margin-left: auto; }
/* On a narrow list the controls drop under the text rather than past the edge. */
.find-list li.find-row { flex-wrap: wrap; }
.find-list li.find-row .find-row__text { flex: 1 1 10rem; }
.find-row__qty { width: 4.5rem; }
.find-list li.find-row .find-row__override { white-space: nowrap; }
/* All, Artworks, Merchandise: radio buttons that look like the task chips. The
   radio is stretched over its label and hidden, so a keyboard still reaches it. */
.kind-chips { display: inline-flex; flex-wrap: wrap; gap: var(--space-2); border: 0; margin: 0; padding: 0; align-self: flex-end; }
.kind-chip { position: relative; display: inline-flex; cursor: pointer; }
.kind-chip input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.kind-chip span {
  font-size: var(--text-small);
  color: var(--ink-muted);
  background: var(--wash);
  border: 1px solid var(--line);
  padding: var(--space-1) var(--space-3);
}
.kind-chip:hover span { color: var(--ink); border-color: var(--ink); background: var(--paper); }
.kind-chip input:checked + span { color: var(--ink); border-color: var(--ink); background: var(--paper); font-weight: 600; }
.kind-chip input:focus-visible + span { outline: 2px solid var(--ink); outline-offset: 2px; }

/* A picker (src/ui/picker.tsx): a search box until something is chosen, then
   the chosen line with a Change button in its place. */
.picker__chosen:empty { display: none; }
.picker__chosen { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-2); border: 1px solid var(--line); background: var(--paper); }
/* What is chosen takes the width and wraps its badges under its words; the Change button drops to the right end rather than leave the title a column of 32px. */
.picker__chosen > .find-row { flex: 1 1 16rem; }
.picker__chosen > .btn { margin-left: auto; }
.find-row--chosen { flex-wrap: wrap; }
.find-row--chosen .find-row__text { flex: 1 1 14rem; }
.picker--chosen .picker__search { display: none; }
/* Orientation and Size beside a picker's box (src/ui/shape-filters.tsx): two small selects that wrap under it on a phone. */
.picker__filters { margin-top: var(--space-2); align-items: flex-end; }
.picker--chosen .picker__filters { display: none; }
.field--compact { margin-bottom: 0; }

/* The slideshow's works (src/ui/hero-picker.tsx): a numbered list of the chosen works, each with Up, Down and Remove, then the search box that adds one. */
.hero-picker__list { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: grid; gap: var(--space-2); counter-reset: hero; }
.hero-picker__item { counter-increment: hero; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2); border: 1px solid var(--line); background: var(--paper); }
.hero-picker__item::before { content: counter(hero); flex: none; width: 1.5rem; text-align: center; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.hero-picker__item .find-row { flex: 1; min-width: 0; }
.hero-picker__actions { display: inline-flex; flex: none; gap: var(--space-2); }
@media (max-width: 40rem) {
  .hero-picker__item { flex-wrap: wrap; }
  .hero-picker__actions { margin-left: 1.5rem; }
}
.field--compact select { min-width: 8rem; }

/* ------------------------------------------------------ An artwork line -- */
/* One work as every list shows it (src/ui/artwork-line.tsx): its picture,
   then the title over the artist, the print's label and its code. A work with
   no picture gets a plain square the same size, never a broken image. */
.artwork-line { display: inline-flex; align-items: center; gap: var(--space-3); min-width: 0; vertical-align: middle; }
.artwork-line__thumb { flex: none; width: 2.5rem; height: 2.5rem; object-fit: cover; background: var(--wash); border: 1px solid var(--line); }
.artwork-line__thumb--none { display: inline-block; }
.artwork-line__text { display: grid; min-width: 0; }
.artwork-line__meta { font-size: var(--text-small); color: var(--ink-muted); }

/* On paper: the medium variant at about 25 mm, so it prints cleanly. */
.artwork-line--print { gap: 4mm; align-items: flex-start; }
.artwork-line--print .artwork-line__thumb { width: 25mm; height: 25mm; object-fit: contain; background: #fff; }

/* The works on one sale, in a list that is one row per sale. */
.thumb-strip { display: inline-flex; gap: 2px; align-items: center; vertical-align: middle; }
.thumb-strip__thumb { width: 1.75rem; height: 1.75rem; object-fit: cover; background: var(--wash); border: 1px solid var(--line); display: inline-block; }
.thumb-strip__more { font-size: var(--text-small); color: var(--ink-muted); margin-left: var(--space-1); }
@media print { .artwork-line__thumb, .thumb-strip__thumb { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }

/* The lay-by builder at the till (batch 5): the switch shows the column it
   uses. A browser without :has() shows both, and the server reads the one
   the switch names. */
.layby-builder { margin-bottom: var(--space-4); }
.layby-builder summary { cursor: pointer; }
.layby-builder:has(input[name="lay_mode"][value="percent"]:checked) .layby-amount,
.layby-builder:has(input[name="lay_mode"][value="default"]:checked) .layby-percent,
.layby-builder:has(input[name="lay_mode"][value="amount"]:checked) .layby-percent { display: none; }
/* The lay-by screen is two sections (B14 item 8): the plan above, and the payment taken today, each with its own heading, a one-line summary and a summary box. */
.layby-facts { margin: var(--space-3) 0; }
.layby-section { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 2px solid var(--ink); }
.layby-section h3 { margin: 0 0 var(--space-2); }
.layby-summary { margin: var(--space-3) 0; padding: var(--space-3) var(--space-4); border: 2px solid var(--ink); background: var(--wash); }

/* Spread remaining balance on a lay-by's Edit schedule form (B12 item 2, public/layby-spread.js). Built hidden by the server and shown by the script, so
   [hidden] is restated. The preview is a quiet box under the button, and a paid row is muted so the rows that change stand out. */
.layby-spread { margin: var(--space-4) 0; }
.layby-spread[hidden], .layby-spread__preview[hidden] { display: none; }
.layby-spread .cluster { align-items: center; }
.layby-spread [data-spread-status]:empty { display: none; }
.layby-spread__preview { margin-top: var(--space-3); padding: var(--space-4); border: 1px solid var(--line); background: var(--wash); }
.layby-spread__preview:focus { outline: 2px solid var(--ink); outline-offset: 2px; }
.layby-spread__title { margin: 0 0 var(--space-2); font-size: 1rem; }
.layby-spread__paid { color: var(--ink-muted); }

/* ------------------------------------------------------- Activity --- */
/* The activity timeline (src/ui/activity-timeline.tsx), stage 15: grouped
   by hour, one row per entry — time, who, the line, the amount, Open.
   Notable and diary entries carry a word as well as a rule down the left,
   so neither depends on colour. */
.activity-dates { margin-bottom: var(--space-4); }
.activity-dates form { gap: var(--space-2); flex-wrap: nowrap; }
.activity-dates input[type="date"] { width: auto; }
.activity-dates .btn--small { min-height: var(--control-height); } /* the day's buttons are the height of the day's box */
.activity-controls { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-end; margin-bottom: var(--space-4); }
.activity-controls .filters { flex: 1 1 40rem; margin-bottom: 0; }
.activity-system { display: flex; gap: var(--space-2); align-items: center; }
.activity-diary textarea { min-height: 3.5rem; }

.activity__day { font-size: var(--text-section); margin: var(--space-6) 0 var(--space-2); }
.activity__hour { margin-bottom: var(--space-4); }
.activity__hour-label {
  font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--ink-muted); font-weight: 500; margin: 0 0 var(--space-1);
  padding-bottom: var(--space-1); border-bottom: 1px solid var(--line);
}
.activity__list { list-style: none; margin: 0; padding: 0; }
.activity__entry {
  display: grid; grid-template-columns: 3.25rem 2.25rem minmax(0, 1fr) auto auto;
  gap: var(--space-2) var(--space-3); align-items: start;
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--line); border-left: 3px solid transparent;
}
.activity__entry--notable { border-left-color: var(--ink); }
.activity__entry--diary { border-left-color: var(--sage); background: var(--sage-wash); }
.activity__time { font-variant-numeric: tabular-nums; color: var(--ink-muted); font-size: var(--text-small); padding-top: 2px; }
.activity__badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border: 1px solid var(--line); background: var(--wash);
  font-size: var(--text-label); letter-spacing: var(--tracking-label); color: var(--ink);
}
.activity__who { display: inline-flex; padding-top: 2px; }
.activity__badge--system { width: auto; min-width: 2rem; padding: 0 var(--space-1); border-style: dashed; color: var(--ink-muted); font-size: 10px; text-transform: uppercase; }
.activity__line { margin: 0; }
.activity__meta { margin: 0; font-size: var(--text-small); color: var(--ink-muted); }
.activity__flag { color: var(--ink); font-weight: 500; }
.activity__gone { color: var(--ink-muted); text-decoration: line-through dotted; text-decoration-thickness: 1px; }
.activity__amount { font-variant-numeric: tabular-nums; white-space: nowrap; padding-top: 2px; }
.activity__open { min-width: 3.5rem; text-align: right; }
.activity__diary-actions { margin-top: var(--space-2); gap: var(--space-2); }
.activity__edit summary { list-style: none; display: inline-flex; }
.activity__edit summary::-webkit-details-marker { display: none; }
.activity__edit[open] { flex-basis: 100%; }
.activity__edit form { margin-top: var(--space-2); }

/* A phone: the amount and Open drop under the line. */
@media (max-width: 640px) {
  .activity__entry { grid-template-columns: 3rem 2.25rem minmax(0, 1fr); }
  .activity__amount { grid-column: 3; padding-top: 0; }
  .activity__open { grid-column: 3; text-align: left; }
}

/* The printed day sheet: one page, small type, no chrome. */
.day-sheet { max-width: 60rem; margin: 0 auto; padding: var(--space-6); }
.day-sheet__title { font-size: var(--text-title); margin: 0 0 var(--space-1); }
.day-sheet__lede, .day-sheet__foot { color: var(--ink-muted); font-size: var(--text-small); }
.day-sheet__table { width: 100%; border-collapse: collapse; font-size: var(--text-small); }
.day-sheet__table th, .day-sheet__table td { padding: 3px 6px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
.day-sheet__table th.money, .day-sheet__table td.money { text-align: right; }
.day-sheet__table tr.is-notable td { font-weight: 600; }
.day-sheet__table tr.is-diary td { font-style: italic; }
@media print {
  .day-sheet { padding: 0; max-width: none; }
  .day-sheet__table { font-size: 8.5pt; }
  .day-sheet__table th, .day-sheet__table td { padding: 1px 4px; border-bottom-color: #bbb; }
  .activity__entry--diary { background: transparent; }
  .activity__open, .activity__diary-actions { display: none; }
}

/* ------------------------------------------------------- Help (stage 17) -- */

/* The "?" in every header: the manual's page for this screen, in a new tab. A circle, not a word, with the word beside it for the front page. */
.help-button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; border: 1px solid var(--ink); border-radius: 50%;
  font-size: var(--text-small); font-weight: 600; line-height: 1; text-decoration: none; color: var(--ink);
}
.help-button:hover, .help-button:focus-visible { background: var(--ink); color: var(--paper); }
.help-link { font-size: var(--text-small); color: var(--ink-muted); }
@media print { .help-button, .help-link { display: none; } }

/* What is new in the manual, and the first-day offer: small, dismissible, and never in the way of the work. */
.help-banner {
  display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap;
  border: 1px solid var(--sage); background: var(--sage-wash); padding: var(--space-3) var(--space-4); margin: 0 0 var(--space-4);
  font-size: var(--text-small);
}
.help-banner__words { flex: 1 1 18rem; }
.help-banner form { margin: 0; }

/* ------------------------------------------------ Comparing two texts -- */

/* Marketing and privacy, a text's two versions word by word (stage 13, slice 5).
   Colour is never the only signal: an addition is underlined and a removal is
   struck through, so the comparison reads in print and in greyscale. */
.diff {
  white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6;
  border: 1px solid var(--line); background: var(--paper); padding: var(--space-4);
}
.diff ins, ins.diff-key {
  background: var(--sage-wash); text-decoration: underline;
  text-decoration-thickness: 2px; text-underline-offset: 0.15em;
}
.diff del, del.diff-key {
  background: var(--danger-wash); color: var(--danger); text-decoration: line-through;
}
@media print {
  .diff ins, .diff del, ins.diff-key, del.diff-key { background: transparent; color: #000; }
}

/* ------------------------------------------------------ The quote sheet -- */

/*
 * The printed quote and the customer's page (the quotes amendment, slice Q2): one document in two places.
 *
 * It borrows the receipt's tokens (black on white, the work at about 25 mm, tables with a hairline under each row) and adds what a quote needs that a
 * receipt does not: a header row that repeats on every page when the lines run over, a line that is never cut across two pages, and totals, notes and
 * terms that stay together. A4 and a 14 mm margin are asked for by the page itself (an @page rule in a style of its own), never here, so a receipt
 * printed from the till keeps the paper it was made for.
 */
.quote-sheet {
  max-width: 190mm; margin: 0 auto; padding: var(--space-6);
  background: var(--paper); border: 1px solid var(--line);
}
.quote-sheet__head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-6); margin-bottom: var(--space-6); }
.quote-sheet__name { margin: 0 0 var(--space-1); font-size: var(--text-h2); font-weight: 600; }
.quote-sheet__id { text-align: right; }
.quote-sheet__title { margin: 0 0 var(--space-1); font-size: var(--text-title); letter-spacing: 0.08em; text-transform: uppercase; }
.quote-sheet__standing { margin: 0 0 var(--space-6); padding: var(--space-4); border: 1px solid var(--ink); }
.quote-sheet__standing-head { margin: 0 0 var(--space-1); font-weight: 600; }
.quote-sheet__for { margin: 0 0 var(--space-5); }
.quote-sheet__lines { width: 100%; border-collapse: collapse; }
.quote-sheet__lines th, .quote-sheet__lines td { padding: var(--space-3) var(--space-3); vertical-align: top; border-bottom: 1px solid var(--line); text-align: left; }
.quote-sheet__lines th.money, .quote-sheet__lines td.money { text-align: right; white-space: nowrap; }
.quote-sheet__item { display: flex; align-items: flex-start; gap: var(--space-3); }
.quote-sheet__words { display: grid; gap: 1px; min-width: 0; }
.quote-sheet__thumb { flex: none; width: 25mm; height: 25mm; object-fit: contain; background: #fff; border: 1px solid var(--line); }
.quote-sheet__totals { margin: var(--space-5) 0 0 auto; max-width: 34rem; width: 100%; }
.quote-sheet__totals th { font-weight: 400; width: auto; }
.quote-sheet__totals th strong { font-weight: 600; }
.quote-sheet__hold { margin: var(--space-5) 0 0; }
.quote-sheet__notes, .quote-sheet__terms { margin-top: var(--space-6); }
.quote-sheet__notes h2, .quote-sheet__terms h2 { font-size: var(--text-h3); margin-bottom: var(--space-2); }
.quote-sheet__foot { margin: var(--space-6) 0 0; padding-top: var(--space-3); border-top: 1px solid var(--line); }

/* The customer's page has no application around it: a quiet bar of what can be done, then the sheet. */
.quote-public { max-width: 200mm; margin: 0 auto; padding: var(--space-6) var(--space-4); }
.quote-public__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-5); }
.quote-public__actions button { cursor: pointer; }
.quote-public__contact { margin-top: var(--space-5); }

@media (max-width: 40rem) {
  .quote-sheet { padding: var(--space-4); }
  .quote-sheet__head { flex-direction: column; }
  .quote-sheet__id { text-align: left; }
  .quote-sheet__thumb { width: 18mm; height: 18mm; }
}

@media print {
  .quote-sheet { border: 0; padding: 0; max-width: none; }
  .quote-sheet__standing { border: 1px solid #000; }
  .quote-sheet__lines thead { display: table-header-group; }
  .quote-sheet__lines tr, .quote-sheet__totals, .quote-sheet__notes, .quote-sheet__terms, .quote-sheet__hold, .quote-sheet__standing { break-inside: avoid; }
  .quote-sheet__thumb { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  .quote-public { padding: 0; max-width: none; }
  .quote-public__actions { display: none !important; }
}

/* ---------------------------------------------------------- Staff roster -- */
/* Stage 18, slice R2: the roster calendar (a month grid and a week list), the warnings panel, the day panel, My roster and the availability forms.
   Built from the tokens above and the existing badge, panel and form styles. Colour is never the only signal: a chip carries the person's category as a
   letter and a different border (solid, double, dashed), a draft an asterisk, an event duty a double outline, and every flag is a word with a shape before it. */

.roster-toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-3) var(--space-6); margin-bottom: var(--space-4); }
.roster-toolbar__title { font-size: var(--text-title); letter-spacing: var(--tracking-title); min-width: 11ch; text-align: center; }
.roster-views { display: inline-flex; }
.roster-views .btn[aria-current='page'] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
/* The toolbar's buttons are as tall as a box (as the page header's are), so the buttons and the person list beside them stand on one line at one height. */
.roster-toolbar .btn--small { min-height: var(--control-height); padding: 0 var(--space-4); font-size: var(--text-small); }
.roster-filter select { width: auto; }

/* The calendar and the panel for a picked day: side by side where there is room, the panel below on a tablet and a phone. */
.roster-layout { display: grid; gap: var(--space-6); grid-template-columns: minmax(0, 1fr); align-items: start; margin-bottom: var(--space-6); }
@media (min-width: 80rem) { .roster-layout--open { grid-template-columns: minmax(0, 1fr) 25rem; } }
.roster-layout .panel { margin-bottom: 0; }

.roster-warnings__list { margin: 0; padding-left: var(--space-5); }
.roster-warnings__list li { margin: var(--space-1) 0; }
.roster-advice { margin: 0; padding-left: var(--space-5); }

.roster-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
.roster-grid th {
  font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; font-weight: 500;
  color: var(--ink-muted); text-align: left; padding: var(--space-2); border-bottom: 1px solid var(--line);
}
.roster-grid td { vertical-align: top; border: 1px solid var(--line); height: 7.5rem; padding: var(--space-2) 0.375rem; background: var(--paper); overflow: hidden; }
.roster-day--outside { background: var(--wash); color: var(--ink-muted); }
.roster-day--past .roster-day__num { color: var(--ink-muted); }
.roster-day--today { outline: 2px solid var(--ink); outline-offset: -2px; }
.roster-day--selected { background: var(--sage-wash); }
.roster-day--closed { background: var(--wash); border-top: 3px solid var(--danger); }
.roster-day__head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-1); flex-wrap: wrap; }
.roster-day__num { font-weight: 500; text-decoration: none; padding: 0 var(--space-1); }
.roster-day__num:hover { text-decoration: underline; }
.roster-day__weekday { display: none; }
.roster-day__flags { display: flex; flex-wrap: wrap; gap: 2px; justify-content: flex-end; min-width: 0; }
.roster-day__flags .badge { max-width: 100%; overflow: hidden; text-overflow: ellipsis; padding: 0 var(--space-1); }
.roster-flag {
  font-size: var(--text-label); letter-spacing: var(--tracking-label); padding: 0 var(--space-1); max-width: 100%;
  border: 1px solid var(--line); background: var(--paper); color: var(--ink-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.roster-flag::before { margin-right: 0.25em; }
.roster-flag--holiday::before { content: '\2605'; }
.roster-flag--closed { color: var(--danger); border-color: var(--danger); background: var(--danger-wash); }
.roster-flag--closed::before { content: '\2715'; }
.roster-flag--special::before { content: '\25C7'; }
.roster-event { text-decoration: none; }
.roster-off { font-size: var(--text-small); color: var(--ink-muted); margin: var(--space-1) 0 0; }

.roster-chips { list-style: none; margin: var(--space-1) 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.roster-chip {
  display: inline-flex; align-items: center; gap: var(--space-1); max-width: 100%; overflow: hidden;
  font-size: var(--text-small); line-height: 1.3; padding: 1px var(--space-2) 1px 0;
  border: 1px solid var(--ink-muted); background: var(--paper); color: var(--ink);
}
.roster-chip__mark {
  display: inline-flex; align-items: center; justify-content: center; align-self: stretch; min-width: 1.4em;
  font-size: 0.75em; font-weight: 600; background: var(--ink); color: var(--paper);
}
.roster-chip__name { overflow-wrap: break-word; } /* a name wraps in its chip: nothing is cut, so nothing needs a title to say what it was */
/* In the month grid a chip is a bar the width of its cell: the mark and the name stand together (the name wraps between words), and a part of the day that has no room beside them goes under. */
.roster-grid .roster-chip { display: flex; flex-wrap: wrap; row-gap: 0; }
.roster-grid .roster-chip__name { flex: 1 1 0; }
.roster-chip__part { font-size: var(--text-label); letter-spacing: var(--tracking-label); color: var(--ink-muted); white-space: nowrap; }
.roster-chip--owner_director { border: 2px solid var(--ink); background: var(--sage-wash); }
.roster-chip--staff { border: 1px solid var(--sage); background: var(--paper); }
.roster-chip--staff .roster-chip__mark { background: var(--sage); color: var(--ink); }
.roster-chip--casual { border: 1px dashed var(--ink-muted); background: var(--wash); }
.roster-chip--casual .roster-chip__mark { background: var(--ink-muted); }
.roster-chip--event { border-style: double; border-width: 3px; }
/* A draft (2 November 2026): hatched, with the word Draft in it, so a day nobody else can see yet is plain on the calendar in greyscale and in print. */
.roster-chip--draft { background-image: repeating-linear-gradient(135deg, transparent 0 4px, rgb(20 20 20 / 9%) 4px 5px); }
.roster-chip__draft {
  font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; font-weight: 600; line-height: 1.3;
  padding: 0 0.25em; border: 1px solid currentColor; white-space: nowrap; background: var(--paper);
}

.roster-week { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.roster-week__day { border: 1px solid var(--line); padding: var(--space-3) var(--space-4); background: var(--paper); }
.roster-week__head { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-2); }
.roster-week__head a { text-decoration: none; }
.roster-week__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.roster-legend { margin: var(--space-3) 0 0; line-height: 2; }
.roster-legend .roster-chip { vertical-align: middle; }

.roster-panel h3 { margin: var(--space-6) 0 var(--space-2); }
.roster-panel__flags { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.roster-entries { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.roster-entry { border-top: 1px solid var(--line); padding: var(--space-3) 0; }
.roster-entry details { margin-top: var(--space-2); }
.roster-entry details form { margin-top: var(--space-3); }
.roster-picker { border: 0; padding: 0; margin: 0 0 var(--space-4); }
.roster-picker__off label { color: var(--ink-muted); }
.roster-picker__off label > span { font-style: italic; }
.roster-day-forms { display: grid; gap: var(--space-3); }
/* Each change to the day is a box over its button, which is at the right end (a button with no box is alone at the right end). */
.roster-day-form { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2); }
.roster-day-form > input[type='text'] { align-self: stretch; }

/* My roster: a rostered day is a solid block that says You, a day the person cannot work is dotted with a cross, and a closed day is washed with the word Closed. */
.my-day__me { margin: var(--space-1) 0 0; padding: 1px 0.1875rem; background: var(--ink); color: var(--paper); font-size: var(--text-small); }
.my-day__with { margin: var(--space-1) 0 0; color: var(--ink-muted); }
.my-day__off-mark { margin: var(--space-1) 0 0; padding: 1px var(--space-2); border: 2px dotted var(--ink); font-size: var(--text-small); }
.my-day--off { border: 2px dotted var(--ink-muted); }
.my-key { display: inline-block; padding: 0 var(--space-2); border: 1px solid var(--line); }
.my-key--on { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.my-key--off { border: 2px dotted var(--ink); }
.my-key--closed { background: var(--wash); border-top: 3px solid var(--danger); }

/* Availability forms and the list of what is marked. */
.avail__days { border: 0; padding: 0; margin: 0 0 var(--space-4); display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); }
.avail__days legend { font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); margin-bottom: var(--space-2); padding: 0; }
.avail__list { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.avail__list li { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); flex-wrap: wrap; padding: var(--space-2) 0; border-top: 1px solid var(--line); }
.bulk-report ul { margin: 0 0 var(--space-3); padding-left: var(--space-5); }

/* Attendance: the review table's headings wrap rather than set the width of a column, and its Recorded cell has room for a sentence (40 characters stood in 94px, five lines tall). The reason box
   above it stands over its button, which is at the right end; the Recheck's note is at the left of its button. */
.attendance-days thead th { white-space: normal; }
.attendance-days th, .attendance-days td { padding-inline: var(--space-2); }
.attendance-days td[data-label='Recorded'] { min-width: 14rem; }
@media (max-width: 60rem) { .attendance-days td[data-label='Recorded'] { min-width: 0; } .attendance-days td { padding-inline: var(--space-3); } }
.attendance-bulk { margin-bottom: var(--space-4); }
.attendance-bulk .form-actions { margin-top: var(--space-3); }
.roster-note-action { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-2) var(--space-4); margin-top: var(--space-4); }
.roster-note-action > span { flex: 1 1 18rem; }
/* A pair of buttons written primary first (so that Enter in the form's box does what it always did) is drawn with the primary at the right end, the other to its left. */
.form-actions.roster-primary-last { flex-direction: row-reverse; justify-content: flex-start; }

/* Six tabs do not fit a phone in one row: they wrap, each keeping its underline. */
.tabs--wrap { flex-wrap: wrap; gap: 0 var(--space-5); }

/* A tablet and a phone have no room for seven columns (105px each at 768, too narrow for a name or for a day number to press): each day is a row, and the days from the next or the last month are left out (on screen only: a page printed keeps its grid).
   Every link in a row (the day, an event, a week's heading) is a box 40px high. */
@media screen and (max-width: 48rem) {
  .roster-grid thead { display: none; }
  .roster-grid, .roster-grid tbody, .roster-grid tr, .roster-grid td { display: block; width: 100%; }
  .roster-grid td { height: auto; min-height: 0; }
  .roster-grid td.roster-day--outside { display: none; }
  .roster-day__weekday { display: inline; }
  .roster-day__head { align-items: center; }
  .roster-day__num { display: inline-flex; align-items: center; gap: var(--space-1); min-width: 2.5rem; min-height: 2.5rem; padding: 0 var(--space-2); }
  .roster-grid .roster-chip { display: inline-flex; flex-wrap: nowrap; }
  .roster-grid .roster-chip__name { flex: 0 1 auto; }
  .roster-day__flags a.badge, .roster-panel__flags a.badge { min-height: 2.5rem; }
  .roster-week__head a { display: inline-flex; align-items: center; min-height: 2.5rem; }
}
@media (max-width: 40rem) {
  .roster-toolbar__title { min-width: 0; }
}

/*
 * The roster's publish bar (B12, item 3; `PublishBar` in src/ui/staff-roster-calendar.tsx): the status and the Publish roster / Publish changes button, always on the screen. Sticky at the foot of
 * the page like the Save bar (B11), and for the same reasons: solid, a rule above it, safe-area padding. It has a height of its own (`--roster-publish-bar-h`, plus the home indicator's inset) so
 * that the Save bars of the day panel's forms, which are sticky at the foot too, can sit exactly above it instead of under it: `.form-action-bar` is lifted by that height on this page only.
 * The status sits over its sentence, and what does not fit is cut to two lines and is in full in the title and in the warnings panel above the calendar. Below 64rem the quiet
 * Other dates link goes (the Publish tab is one tap away), and on a phone the sentence goes too and the status wraps beside the button, so the bar is one thumb-high row.
 */
html:has(.roster-publish-bar) { --roster-publish-bar-h: 5rem; }
.roster-publish-bar {
  position: sticky; bottom: 0; z-index: 25;
  box-sizing: border-box; height: calc(var(--roster-publish-bar-h, 5rem) + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4);
  margin-top: var(--space-6);
  padding: var(--space-2) var(--space-4);
  padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
  padding-left: calc(var(--space-4) + env(safe-area-inset-left, 0px));
  padding-right: calc(var(--space-4) + env(safe-area-inset-right, 0px));
  background: var(--paper);
  border-top: 2px solid var(--ink);
}
.roster-publish-bar::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 100%; height: var(--space-4);
  background: linear-gradient(to top, rgb(20 20 20 / 7%), rgb(20 20 20 / 0%));
  pointer-events: none;
}
.roster-publish-bar__status { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); min-width: 0; flex: 1 1 auto; }
.roster-publish-bar__status .badge { flex: none; }
.roster-publish-bar__detail {
  min-width: 0; font-size: var(--text-small); color: var(--ink-muted); line-height: 1.3;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.roster-publish-bar__buttons { display: flex; gap: var(--space-2); align-items: center; flex: none; }
.roster-publish-bar .btn[disabled] { opacity: 0.45; cursor: not-allowed; }
/* The notice above the bar when a form on the page has changes that are not saved: it floats over the page and does not change the bar's height. */
.roster-publish-bar__warn { position: absolute; left: var(--space-4); right: var(--space-4); bottom: calc(100% + var(--space-2)); margin: 0; }
.roster-publish-bar__warn[hidden] { display: none; }
/* The Save bars of the day panel's forms sit above the publish bar, not under it. */
html:has(.roster-publish-bar) .form-action-bar { bottom: calc(var(--roster-publish-bar-h) + env(safe-area-inset-bottom, 0px)); }
html:has(.roster-publish-bar) { scroll-padding-bottom: calc(6rem + var(--roster-publish-bar-h)); }
.roster-confirm__part { margin: var(--space-6) 0 var(--space-3); }
/* The confirm step's button names what it does and how many it writes to ("Publish the roster and write to 5 people", whose words the manual quotes): on a phone the two buttons are stacked, the width of the card, and the words are set to fit one line. */
@media (max-width: 40rem) {
  .roster-confirm-form { padding-inline: var(--space-3); }
  .roster-confirm-form .form-actions { flex-direction: column; align-items: stretch; }
  .roster-confirm-form .form-actions .btn { padding-inline: var(--space-2); letter-spacing: 0; font-size: var(--text-label); }
}
@media (max-width: 64rem) { .roster-publish-bar__buttons .btn--quiet { display: none; } }
/* On a phone the status stands on its own line (a badge's words never wrap: "Published — unpublished changes" is a line of 270px) and the button takes the line under it, the width of the bar. */
@media (max-width: 40rem) {
  html:has(.roster-publish-bar) { --roster-publish-bar-h: 6rem; }
  .roster-publish-bar { flex-direction: column; align-items: stretch; justify-content: center; gap: var(--space-2); }
  .roster-publish-bar__status { flex: none; }
  .roster-publish-bar__detail { display: none; }
  .roster-publish-bar__buttons { justify-content: flex-end; }
  .roster-publish-bar__buttons .btn { flex: 1 1 auto; }
}
@media print { .roster-publish-bar { display: none; } }

/*
 * The reminder at the top of the roster (2 November 2026; `PublishBanner` and `PublishOffer` in src/ui/staff-roster-calendar.tsx): an amber banner with the Publish button in it while something waits in the
 * dates on show, the message after a save with Publish now in it, and a quiet line when it waits only on other dates. It is the help banner's layout (words, then the button) with the warning colours; on a
 * phone the button takes the width of the banner, so it is one thumb away at the top as the bar is at the foot.
 */
.roster-publish-banner { border-color: var(--warn); border-left-width: 3px; background: var(--warn-wash); }
.roster-publish-banner__words { display: flex; flex-direction: column; gap: var(--space-1); }
.roster-publish-offer { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.roster-publish-offer__words { flex: 1 1 18rem; }
.roster-publish-banner__warn, .roster-publish-offer__warn { flex: 1 1 100%; margin: 0; }
.roster-publish-banner__warn[hidden], .roster-publish-offer__warn[hidden] { display: none; }
.roster-publish-quiet { margin: 0 0 var(--space-4); }
@media (max-width: 40rem) { .roster-publish-banner > .btn, .roster-publish-offer > .btn { flex: 1 1 100%; } }
@media print { .roster-publish-banner, .roster-publish-offer, .roster-publish-quiet { display: none; } }

/* Discount codes (stage 19, slice 2): the seven steps of the wizard as a row that wraps, a list with no bullets, and a form that sits in a line of text. */
.steps { margin: 0 0 var(--space-5); }
.steps__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); font-size: var(--text-small); }
.steps__list li[aria-current='step'] { font-weight: 600; border-bottom: 2px solid var(--ink); }
/* A touch screen presses a step: each is a box 40px high (the words are one line, 15px). */
@media (max-width: 48rem) { .steps__list a, .steps__list li > span { display: inline-flex; align-items: center; min-height: 2.5rem; } }
.plain-list { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.plain-list li { padding: var(--space-2) 0; border-top: 1px solid var(--line); }
form.inline { display: inline; margin-left: var(--space-2); }

/* A masked bank account with its Reveal beside it, and the Change box under it on an artist's edit form (B11 stage 3, 10 October 2026). A masked number never wraps inside itself. */
.bank-reveal { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
[data-bank-change-panel] { margin-top: var(--space-2); max-width: 22rem; }

/* Pay artists (27 October 2026): the payments to make by hand. A Copy button is built hidden and shown by the script, and `.btn` sets its own display, so `hidden` is restated. A number never wraps inside itself. */
.pay-copy[hidden], [data-pay-tools][hidden], [data-pay-tickall-label][hidden] { display: none !important; }
.pay-table td { vertical-align: top; }
.pay-table .mono { white-space: nowrap; }
.pay-table .pay-copy { margin-left: var(--space-1); }
.pay-table input[type='checkbox'] { width: 1.25rem; height: 1.25rem; }
.pay-mark { margin-top: var(--space-6); max-width: 28rem; }
/* The print view is a tick sheet: the payments and an empty box for each, with no buttons and no form controls but the boxes. */
@media print {
  .pay-copy, .pay-mark, [data-pay-tools] { display: none !important; }
  .pay-table td, .pay-table th { padding: 0.25rem 0.4rem; font-size: 10pt; }
}

/* Confirming again with a passkey (passkeys slice C2, 11 October 2026): the quiet way to use the password instead, and the line that says what the passkey prompt is doing. */
.step-up-fallback { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--line); max-width: 24rem; }
.step-up-fallback > summary { cursor: pointer; color: var(--ink-muted); font-size: var(--text-small); }
.step-up-fallback[open] > summary { margin-bottom: var(--space-3); }
.step-up-status { min-height: 1.5em; margin: var(--space-3) 0 0; color: var(--ink-muted); }
.step-up-status--error { color: var(--danger); }

/* Passkeys (amendment stage 3, 11 October 2026): the account security page, the sign-in button and the one-time offer. Anything a script reveals is built hidden, and `[hidden]` is restated because these set their own display. */
.passkey-js[hidden], .passkey-note[hidden], .passkey-rename[hidden] { display: none !important; }
.link-button {
  all: unset; cursor: pointer; color: var(--ink); text-decoration: underline;
  text-underline-offset: 0.2em; text-decoration-thickness: 1px; font-size: var(--text-small);
}
.link-button:hover { text-decoration-color: var(--sage); }
.link-button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.passkey-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.passkey-rename { display: inline-flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; margin-top: var(--space-2); }
.passkey-add { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: flex-end; }
.passkey-add .field { margin: 0; flex: 1 1 14rem; }
.passkey-sign-in .btn { width: 100%; }
.passkey-sign-in__help { margin: var(--space-2) 0 0; }
.passkey-status { min-height: 1.5em; margin: var(--space-3) 0 0; }
#add-a-passkey { margin-top: var(--space-8); }

/* Website, Edit pages (src/routes/site-editor.tsx, public/site-editor.js): the website's page in a frame, the places that can be changed listed beside it. */
.site-editor { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; gap: var(--space-6); align-items: start; }
.site-editor__stage { position: relative; border: 1px solid var(--line); border-radius: var(--radius, 6px); background: var(--wash); overflow: hidden; }
.site-editor__frame { display: block; width: 100%; height: calc(100vh - 11rem); min-height: 34rem; border: 0; background: #fff; }
.site-editor__loading { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; pointer-events: none; }
.site-editor__panel { position: sticky; top: var(--space-4); max-height: calc(100vh - 3rem); overflow: auto; border: 1px solid var(--line); border-radius: var(--radius, 6px); padding: var(--space-4); background: var(--paper, #fff); }
.site-editor__field { padding: var(--space-3) 0; border-top: 1px solid var(--line); }
.site-editor__field.is-selected { background: var(--wash); }
.site-editor__field.is-changed { background: rgba(212, 160, 23, 0.12); padding-left: var(--space-3); padding-right: var(--space-3); }
.site-editor__field.is-changed .site-editor__label::after { content: ' · Changed, not saved yet'; font-weight: 400; color: var(--ink-muted); font-size: var(--text-small); }
.site-editor__label { display: block; font-weight: 600; margin-bottom: var(--space-1); }
.site-editor__field textarea, .site-editor__field input[type="text"] { width: 100%; }
.site-editor__team { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem; }
.site-editor__thumb { display: block; max-width: 100%; max-height: 8rem; object-fit: contain; border: 1px solid var(--line); margin-bottom: var(--space-2); background: var(--wash); }
.site-editor__save { position: sticky; bottom: calc(-1 * var(--space-4)); margin: var(--space-4) calc(-1 * var(--space-4)) calc(-1 * var(--space-4)); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--line); background: var(--paper, #fff); }
.site-editor__pictures { width: min(46rem, 94vw); border: 1px solid var(--line); border-radius: var(--radius, 6px); padding: var(--space-5); }
.site-editor__pictures::backdrop { background: rgba(0, 0, 0, 0.45); }
.site-editor__library { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); gap: var(--space-3); max-height: 50vh; overflow: auto; }
.site-editor__option { display: block; width: 100%; padding: 0; border: 1px solid var(--line); background: var(--wash); cursor: pointer; }
.site-editor__option img { display: block; width: 100%; height: 6rem; object-fit: cover; }
@media (max-width: 70rem) { .site-editor { grid-template-columns: 1fr; } .site-editor__panel { position: static; max-height: none; } }
.site-editor__loading[hidden] { display: none; }
.site-editor__field textarea { min-height: 0; resize: vertical; }
.site-editor__field [hidden] { display: none; }
.site-editor__needs { margin: 0 0 var(--space-1); padding: var(--space-1) var(--space-2); background: rgba(212, 160, 23, 0.18); border-left: 3px solid rgb(176, 112, 0); }
.site-editor__seo { margin-top: var(--space-4); padding-top: var(--space-3); border-top: 2px solid var(--line); }
.site-editor__seo[hidden] { display: none; }
.site-editor__seo-heading { margin: 0 0 var(--space-1); }
.site-editor__snippet { margin: var(--space-3) 0; padding: var(--space-3); border: 1px solid var(--line); border-radius: var(--radius, 6px); background: #fff; overflow-wrap: anywhere; }
.site-editor__snippet-url { font-size: var(--text-small); color: var(--ink-muted); }
.site-editor__snippet-title { margin: 2px 0; font-size: 1.1rem; line-height: 1.3; color: #1a0dab; }
.site-editor__snippet-description { font-size: var(--text-small); line-height: 1.45; color: var(--ink-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.site-editor__count { margin: var(--space-1) 0 0; color: var(--ink-muted); }
.site-editor__count.is-off { color: rgb(150, 90, 0); font-weight: 600; }

/* Settings, Public site settings, Search engines and sharing (src/ui/seo-settings-group.tsx): the AI crawler choices and the share picture. */
.seo-crawlers { border: 0; margin: 0 0 var(--space-4); padding: 0; min-width: 0; }
.seo-crawlers legend { padding: 0; margin-bottom: var(--space-2); font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); font-weight: 500; }
.seo-choice { margin-bottom: var(--space-3); }
.seo-choice .check { display: flex; gap: var(--space-3); align-items: flex-start; }
.seo-choice .check input { margin-top: 0.25rem; flex: none; }
.seo-choice .field__help { margin-left: calc(1rem + var(--space-3)); }
.seo-help { margin: 0 0 var(--space-4); }
.seo-share__preview { display: block; max-width: 24rem; width: 100%; aspect-ratio: 1200 / 630; object-fit: cover; margin-top: var(--space-3); border: 1px solid var(--line); background: var(--wash); }
.seo-share__preview[hidden] { display: none; }
.seo-share__upload { margin-top: var(--space-3); }
.seo-share__upload label { display: block; font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); font-weight: 500; margin-bottom: var(--space-1); }

/* Memo notes (B12 item 4, 11 October 2026): the note icons in a list, the pair of boxes, and the badge that says a note is shared with the customer. */
.note-icons { display: inline-flex; gap: var(--space-1); vertical-align: middle; margin-left: var(--space-2); }
.note-icon { display: inline-flex; align-items: center; color: var(--ink-muted); cursor: help; }
.note-icon--shared { color: var(--warn); }
.memo-note-text { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0 0 var(--space-3); }

/* Text a person typed into a multi-line box keeps its line breaks on screen, as it does in an email (17 October 2026). Every newline is kept, so a blank line stays one blank line,
   runs of spaces collapse, a long line wraps. It is escaped text, never HTML: the class changes how the text is laid out and nothing else. */
.pre-line { white-space: pre-line; overflow-wrap: anywhere; }
.badge--shared { color: var(--warn); border-color: var(--warn); background: var(--warn-wash); }
.badge--shared::before { content: '\25D0'; }
.memo-shared textarea { border-color: var(--warn); }

/* ---------------------------------------- Artworks, the gallery view ---- */
/* 14 October 2026 (src/ui/artwork-gallery.tsx, src/routes/artworks.tsx). The grouped list's works as cards: the picture large and never cropped, the words small beneath. */

/* List | Gallery, in the page header beside the other links; List stays marked on the one-line-per-object view. */
.view-switch { display: inline-flex; border: 1px solid var(--ink); background: var(--paper); }
.view-switch a {
  display: inline-flex; align-items: center; min-height: var(--control-height); padding: 0 var(--space-4);
  font-size: var(--text-small); letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--ink); text-decoration: none;
}
.view-switch a + a { border-left: 1px solid var(--ink); }
.view-switch a:hover { background: var(--wash); }
.view-switch a[aria-current] { background: var(--ink); color: var(--paper); }

.gallery-panel { padding: var(--space-4); }
.gallery-sort { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-4); margin-bottom: var(--space-4); font-size: var(--text-small); }

/* Two across on a phone, three on a tablet, four on a laptop, five on a wide screen. minmax(0, 1fr) so a long title wraps and never widens the page. */
.gallery-grid { list-style: none; margin: 0 0 var(--space-4); padding: 0; display: grid; gap: var(--space-6) var(--space-4); grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 40rem) { .gallery-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .gallery-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (min-width: 90rem) { .gallery-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
@media (min-width: 40rem) { .gallery-panel { padding: var(--space-6); } }

.gallery-card { position: relative; display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.gallery-card:has(.gallery-card__title:focus-visible) { outline: 2px solid var(--ink); outline-offset: 4px; }

/* A square tile, the whole picture fitted inside it on a neutral ground (a portrait, a landscape and a round work all read true), or "No photo". */
.gallery-card__tile {
  position: relative; display: grid; place-items: center; aspect-ratio: 1 / 1;
  background: var(--wash); border: 1px solid var(--line); overflow: hidden; text-decoration: none;
}
.gallery-card__tile img { position: absolute; inset: 0; width: 100%; height: 100%; padding: var(--space-2); box-sizing: border-box; object-fit: contain; }
.gallery-card__none { color: var(--ink-muted); font-size: var(--text-small); }

/* The tick for the bulk bar, in the corner on a paper ground so it reads over any picture. */
.gallery-card__pick {
  position: absolute; z-index: 1; top: var(--space-2); left: var(--space-2);
  display: grid; place-items: center; width: 2.5rem; height: 2.5rem; margin: 0;
  background: var(--paper); border: 1px solid var(--line); cursor: pointer;
}
.gallery-card__pick input { margin: 0; }

.gallery-card__body { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); min-width: 0; max-width: 100%; font-size: var(--text-small); overflow-wrap: anywhere; }
.gallery-card__title { font-size: var(--text-body); line-height: var(--leading-tight); }
.gallery-card__flags { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.gallery-card__body .badge { white-space: normal; text-align: left; }
.gallery-card__price { font-size: var(--text-body); font-variant-numeric: tabular-nums; margin-top: var(--space-1); }
.gallery-card__status { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); }

/* ---------------------------------------- No GST: shipping overseas --- */
/* The small control inside the Shipping panel of the Sell screen (16 October 2026): one button and one line, or one line of status and a closed "Which lines". */
.export-gst { margin-bottom: var(--space-4); }
.export-gst__row { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: center; }
.export-gst--on { padding-left: var(--space-3); border-left: 3px solid var(--line); }
.export-gst__lines { margin-top: var(--space-1); }
.export-gst__lines > summary { cursor: pointer; color: var(--ink-muted); }
.export-gst__lines[open] > summary { margin-bottom: var(--space-2); }
.export-gst__lines p, .export-gst__lines ul { margin-top: 0; margin-bottom: var(--space-2); }

/* A framed copy's frame (part F1 of the frame price, 18 October 2026): the extra questions of the Framing control, no panel of their own. They show only
   while Framed is the choice, and only the box for the chosen kind of frame. No script: a browser that cannot do :has() shows the whole fieldset, and the
   service ignores a frame sent for a copy that is not Framed. */
.frame-detail { border: 1px solid var(--line); padding: var(--space-3) var(--space-4); margin: var(--space-4) 0; min-width: 0; }
.frame-detail legend { padding: 0 var(--space-2); font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); }
.frame-detail .check { display: flex; gap: var(--space-3); align-items: flex-start; margin: 0 0 var(--space-2); }
.frame-detail .check input { margin-top: 0.25rem; flex: none; }
.frame-detail__gallery, .frame-detail__included { margin-top: var(--space-3); }
@supports selector(:has(*)) {
  .frame-detail { display: none; }
  form:has(select[name='framing'] option[value='framed']:checked) .frame-detail { display: block; }
  .frame-detail:not(:has(input[name='frame_basis'][value='gallery']:checked)) .frame-detail__gallery { display: none; }
  .frame-detail:not(:has(input[name='frame_basis'][value='included']:checked)) .frame-detail__included { display: none; }
}

/* The gallery's frame as a line of its own beside its artwork (part F2a of the frame price, 18 October 2026): the Sell screen, the sale page and the overseas list draw the frame
   a step in from the artwork above it, so a pair reads as a pair. No script. */
td.frame-row { padding-left: var(--space-5); border-left: 3px solid var(--line); }
.frame-line-words .badge { margin-left: var(--space-1); }
.frame-remove summary, .frame-give-back summary { cursor: pointer; }

/* ------------------------------------------------ Polish pass, 18 October 2026 -- */
/*
 * Layout faults that had the same shape on many screens, found by opening them at 1440, 1024, 768 and 390 pixels. Written last so each rule says in one place
 * what it is for; the reasons are in the comments, and `test/ui-polish-guards.test.ts` holds the ones that must not be lost.
 */

/* The customer list: eleven columns that fit the card at 1440 and scroll inside it (Name and the tick stay put) where they do not. */
.table--customers { font-size: var(--text-small); }
.table--customers th, .table--customers td { padding: var(--space-2) 0.625rem; }
.table--customers td.col-name, .table--customers td.col-email { font-size: var(--text-body); }
.table--customers .col-select { width: 2.75rem; min-width: 2.75rem; max-width: 2.75rem; padding: var(--space-2) 0 var(--space-2) 0.25rem; }
.table--customers .col-name { min-width: 12.5rem; overflow-wrap: break-word; }
.table--customers .col-name .badge { margin-left: var(--space-1); vertical-align: middle; }
/* An address is one word: it keeps to one line and shows its end with a tooltip, rather than breaking into three lines of letters. */
.table--customers .col-email { min-width: 10rem; max-width: 15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.table--customers .col-phone, .table--customers .col-optin, .table--customers .col-date, .table--customers .money { white-space: nowrap; }
.table--customers .money { font-variant-numeric: tabular-nums; }
.table--customers .col-phone .badge, .table--customers .col-lists .badge { display: flex; width: max-content; margin-top: 2px; }
.table--customers .col-lists { min-width: 8rem; max-width: 13rem; }
/* The taste headline (Stage 24, piece 4b): one line that is cut where it is long, the whole sentence in its tooltip and in the Taste panel it links to. On a phone the row is a record and the whole headline is shown. */
.table--customers .col-taste { min-width: 11rem; max-width: 17rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.optin { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.optin__date { font-size: var(--text-label); font-variant-numeric: tabular-nums; }
.list-chip { display: inline-flex; align-items: baseline; gap: var(--space-1); max-width: 100%; color: var(--ink-muted); }
.list-chip__name { min-width: 0; max-width: 10rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-chip__more {
  flex: none; padding: 0 0.35rem; border: 1px solid var(--line); font-size: var(--text-label); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
.lists-full { display: none; overflow-wrap: anywhere; color: var(--ink-muted); }
.money__note { display: block; font-size: var(--text-label); line-height: 1.3; }

/* The name (and the tick before it) stays where it is while the other columns slide under it. */
.table-wrap--sticky .col-select, .table-wrap--sticky .col-name { position: sticky; left: 0; z-index: 1; background: var(--paper); }
.table-wrap--sticky .col-select + .col-name { left: 2.75rem; }
.table-wrap--sticky thead .col-select, .table-wrap--sticky thead .col-name { z-index: 2; background: var(--wash); }
.table-wrap--sticky tbody tr:hover .col-select, .table-wrap--sticky tbody tr:hover .col-name { background: var(--wash); }

/* Under 60rem a row is a record (see .table--collapse): the list shows every list, and nothing sticks. */
@media (max-width: 60rem) {
  .table-wrap--sticky .col-select, .table-wrap--sticky .col-name { position: static; }
  .table--customers .col-name, .table--customers .col-email, .table--customers .col-lists, .table--customers .col-taste { min-width: 0; max-width: none; }
  .table--customers .col-taste { white-space: normal; overflow-wrap: anywhere; text-overflow: clip; }
  .table--customers .col-email { white-space: normal; overflow-wrap: anywhere; text-overflow: clip; }
  .table--customers .col-select { width: auto; max-width: none; }
  .list-chip { display: none; }
  .lists-full { display: block; }
}

/*
 * Touch (a tablet at the counter, a phone). Every link and button in the bar, and every small button on a screen, is at least 44 pixels to press, as the large ones
 * already were (it was 40 until 30 October 2026). The "?" and the count keep the size they are drawn at and are given a bigger area to press by an invisible box around them.
 * Below 64rem the bar is the one short row of the Menu (see "The Menu" above), so it sticks to the top of the screen as it does on a desktop.
 */
@media (pointer: coarse), (max-width: 60rem) {
  .app-nav a { padding-block: 0.6875rem; }
  .wordmark { display: inline-block; padding-block: 0.4375rem; }
  .app-header__who > a:not(.btn, .help-button, .task-badge), .signout button { display: inline-flex; align-items: center; min-height: var(--control-height); padding-inline: 0.125rem; }
  .app-header__who .help-button, .app-header__who .task-badge { position: relative; }
  .app-header__who .help-button::after, .app-header__who .task-badge::after { content: ''; position: absolute; inset: -0.625rem -0.25rem; }
  .btn--small { min-height: var(--control-height); }
  th a { padding-block: 0.75rem; }
}
@media (max-width: 40rem) {
  .page-header__actions { width: 100%; justify-content: flex-start; }
  .page-header__actions > .btn, .page-header__actions > form { flex: 1 1 auto; }
  .page-header__actions > form > .btn { flex: 1 1 auto; }
  /* The page's one primary action (the black button) comes first under the title and is as wide as the screen; the rest follow, two to a row. */
  .page-header__actions > .btn:not(.btn--secondary, .btn--quiet, .btn--danger),
  .page-header__actions > form:has(> .btn:not(.btn--secondary, .btn--quiet, .btn--danger)) { order: -1; flex: 1 1 100%; }
  .page-header__lead .avatar--lg { --s: 3.5rem; } /* the greeting's picture was 96 pixels of a 343 pixel line */
}

/* A tick box on its own (the select column of a list) in a box the size of a fingertip, so it can be pressed. */
.tick-box { display: flex; align-items: center; justify-content: center; min-width: 2.5rem; min-height: 2.5rem; margin: -0.375rem 0; cursor: pointer; }
.tick-box > input { margin: 0; }

/*
 * What the doctype changed (it was missing, so every page was drawn in quirks mode: table text at 16px with a line height of "normal", badges and headings at "normal").
 * A table now reads at the page's own size, and these keep it as compact as it was: a row of a list is not taller for being right.
 */
table { line-height: 1.4; }
.badge { line-height: 1.2; }
thead th { line-height: 1.3; }

/* A tick box or a radio among words sits on the line of the words, not on the one above. */
input[type='checkbox'], input[type='radio'] { vertical-align: -0.2em; }

/* A key and value table: the key can carry a line of help, which is not bold as the key is (a heading cell is bold by default), and the column is wide enough to read it in. */
.table--detail th { width: 14rem; }
th .muted, th .small, th .field__help { font-weight: 400; }

/* A colour box and a file box stand at the height of the boxes beside them (they were drawn by the browser at its own, 31 and 37 pixels, in a row of 44s). */
input[type='color'] { width: 4rem; min-height: var(--control-height); padding: 0.25rem; border: 1px solid var(--control-border); background: var(--paper); border-radius: var(--radius); }
input[type='file'] { min-height: var(--control-height); padding-block: 0.6875rem; }

/* The buttons across the top of a work's page are one row at one height: Sell is the black one and the rest are the same outline, none of them a size of its own. */
.artwork-actions { align-items: center; }
.artwork-actions .btn--large, .artwork-actions__sell select { min-height: var(--control-height); }
.artwork-actions .btn--large { padding: 0 var(--space-6); font-size: var(--text-small); }
.artwork-actions .btn--small { min-height: var(--control-height); padding: 0 var(--space-4); font-size: var(--text-small); }
.artwork-actions .btn--quiet { border-color: var(--ink); background: var(--paper); }
.artwork-actions .btn--quiet:hover { background: var(--wash); }

/* A label with a badge after it (a note shared with the customer) lets the badge drop to its own line rather than hang out of the box. */
.memo-shared > label { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-2); }
/* In a grid of fields the two note boxes each take a whole line, so the badge dropping under "Note for the customer" does not push its box lower than the internal note's. */
.form-grid > .memo-shared, .form-grid > .field:has(+ .memo-shared) { grid-column: 1 / -1; }

/* One row of fields after another is spaced as the fields inside a row are; a row owns the gap between its own fields and had none above the next. */
.row + .row { margin-top: var(--space-3); }
.choice + :is(.row, .form-grid, .field), .check-row + :is(.row, .form-grid, .field), .form-grid + :is(.row, .form-grid, .field) { margin-top: var(--space-4); } /* a tick box line is not a field: what follows it is spaced as a field is */
.row > .row + .row { margin-top: 0; } /* two rows side by side (a form that is a row, beside another) start on one line */
/* The kind chips (All, Artworks, ...) are as tall as the boxes beside them, so they sit on the same line, and are a size a finger can press. */
.kind-chip span { display: inline-flex; align-items: center; min-height: var(--control-height); }

/* A note that belongs to the whole grid of fields (the country's note under an address) is a line of its own, the width of the grid, and is drawn closer to the box it is about. */
.form-grid > .field__help { grid-column: 1 / -1; margin-top: calc(-1 * var(--space-2)); }

/* A disclosure standing in a row of them (Decline, Cancel) takes room for the form it opens, instead of shrinking to its summary and letting the form run out of it. */
.cluster > details[open] { flex: 1 1 24rem; min-width: 0; }

/* A narrow field is narrow, and wide enough for its label to stay on one line (a heading that wrapped to two lines put its box lower than the box beside it). */
.narrow { max-width: 12rem; } /* wide enough for a heading of about twenty letters on one line ("Reorder when down to" wrapped at ten) */
.row--choice > .field.grow { flex-basis: 11rem; } /* one choice per row (the till's payment methods): the choice's words keep their line's room, and the Position box above them, on a phone, rather than what is left of its line */
@media (max-width: 40rem) { .row--choice > .field.grow { flex: 1 1 100%; } }
.field--medium { max-width: 24rem; } /* a field alone on its line, for a heading of about thirty letters and a number or a short phrase (a figure with a long name, such as a limit in percent) */
/* A small button's words are one or two short ones: they do not wrap in a cell that is a little narrow, the column takes the room instead. */
.btn--small { white-space: nowrap; }
/* On a phone a button with a long label wraps inside the width of the screen instead of running off it (a small button's words were held to one line, and "Ask the artist for more information" pushed a work's page 100 pixels sideways, 30 October 2026). */
@media (max-width: 40rem) { .btn, .btn--small { max-width: 100%; white-space: normal; text-align: center; padding-block: var(--space-2); } }

/* A card that is mostly a table of rows (older screens: a panel with a heading, the table, a pager) has the table run edge to edge, as a table card does, and the heading and pager keep their own padding. A key and value table (.table--detail, whose keys start flush left) keeps the card's own padding. 48 pixels of a 1024 pixel screen were padding round the table. */
.panel:has(> table:not(.table--detail)) { padding: 0; }
.panel:has(> table:not(.table--detail)) > :not(table) { margin: 0; padding: var(--space-4) var(--space-6); }
.panel:has(> table:not(.table--detail)) > p + p { padding-top: 0; } /* two paragraphs in such a card are a paragraph apart (16px), not two paddings (32px) */

/* A record on a narrow screen (see .table--collapse): a cell with nothing in it says nothing, rather than a label with a blank beside it. The customer's name stands as the record's title. */
@media (max-width: 60rem) {
  .table--collapse td:empty { display: none; }
  .table--customers td.col-name { justify-content: flex-start; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); font-weight: 500; }
  .table--customers td.col-name::before { content: none; }
}

/* A cell on a narrow screen that holds blocks (a title and what the gallery holds of it, a name and a note) or a box to choose in (a list, a text box) is a block with its label above, as .stacked is, and not a row of three things squeezed side by side (a list beside its label ran 77px out of its card). */
@media (max-width: 60rem) {
  .table--collapse td:has(> div, > p, > ul, > br, > details, > select, > textarea, > input:not([type='checkbox'], [type='radio'], [type='hidden'])) { display: block; text-align: left; }
  .table--collapse td:has(> div, > p, > ul, > br, > details, > select, > textarea, > input:not([type='checkbox'], [type='radio'], [type='hidden']))::before { display: block; margin-bottom: var(--space-1); }
}

/* A card that is a panel with a table straight in it shows, as .table-wrap does, that it scrolls. */
.panel:has(> table), .panel:has(> form > table) {
  background:
    linear-gradient(to right, var(--paper) 30%, rgb(255 255 255 / 0%)) left center / 2rem 100% no-repeat local,
    linear-gradient(to left, var(--paper) 30%, rgb(255 255 255 / 0%)) right center / 2rem 100% no-repeat local,
    linear-gradient(to right, rgb(20 20 20 / 14%), rgb(20 20 20 / 0%)) left center / 0.75rem 100% no-repeat scroll,
    linear-gradient(to left, rgb(20 20 20 / 14%), rgb(20 20 20 / 0%)) right center / 0.75rem 100% no-repeat scroll,
    var(--paper);
}

/* A row whose name opens into its edit form (customer types, event types, services) gives that cell the room for the form while it is open; it had the width of the word. */
.table--collapse td:has(> details[open]) { min-width: 28rem; }
@media (max-width: 60rem) { .table--collapse td:has(> details[open]) { min-width: 0; } }

/* A select in a form that sits in a table cell (a print's status, with its date and Set) is wide enough for the longest thing in it, and not the 11rem a field is by default: its words were cut off at "it has go". */
td > form.row > .field:has(> select), td > .row > .field:has(> select) { flex: 1 1 19rem; }
/* A bare list in a form of buttons in a cell (a user's role, with Change role beside it) takes the room the buttons leave, not the whole line (it was 100% wide, so its button dropped under it). */
td > form.row > select { flex: 1 1 8rem; width: auto; min-width: 0; }

/* A field that follows a row of fields is spaced from it as a field is from a field (the notes box was eight pixels under the row above it). */
.row + .field { margin-top: var(--space-4); }
/* A grid of fields is spaced from what follows it, and a row from a grid, in the same way (a picker under a grid touched it; the discount form's lists touched the box above). */
.form-grid + :is(.field, .form-grid, .row), .row + .form-grid { margin-top: var(--space-4); }

/* A title column keeps room for a title. The artworks list's was squeezed to 145 pixels beside nine other columns, so a title and what the gallery holds of it ran to four lines. */
.table--collapse td[data-label='Title'] { min-width: 12rem; }
@media (max-width: 60rem) { .table--collapse td[data-label='Title'] { min-width: 0; } }
/* A cell with a sentence under its name (the month-end artist: lay-bys not yet due, waiting for MyArt) keeps room to read it: in a table of thirteen columns it was 82px wide and seven lines tall. */
.table--collapse td.col-note { min-width: 14rem; }
@media (max-width: 60rem) { .table--collapse td.col-note { min-width: 0; } }

/* Touch, continued: the eight items are each wide enough to press as well as tall (the short ones, Sell and Reports, were 32 pixels across), the box round the "?" and the count reaches past them on every side, the section bar's links and a disclosure's heading are as tall as the rest, and a tick box is drawn larger. */
@media (pointer: coarse), (max-width: 60rem) {
  .app-header__bar { padding-inline: var(--space-4); }
  .app-nav { gap: 0; }
  .app-nav a { padding-inline: 0.375rem; }
  .app-header__who .help-button::after, .app-header__who .task-badge::after { inset: -0.625rem; }
  .sub-nav__links a { padding-block: 0.6875rem; }
  summary:not(.btn) { padding-block: 0.75rem; }
  input[type='checkbox'], input[type='radio'] { width: 1.5rem; height: 1.5rem; }
}

/* Tabs wrap onto a second row rather than run past the edge of a phone (the event page's six). */
.tabs { flex-wrap: wrap; column-gap: var(--space-5); }
@media (pointer: coarse), (max-width: 60rem) {
  .tabs { column-gap: 0; }
  .tabs a { padding-inline: 0.625rem; }
}

/* The way of paying on the Sell screen is wide enough for "EFTPOS or card on the terminal". */
.row > .field:has(> #p-method) { flex: 2 1 17rem; }
/* A link in a table cell on a touch screen has a box to press that is more than the height of its words. */
@media (pointer: coarse), (max-width: 60rem) {
  td > a:not(.btn) { display: inline-block; padding-block: 0.75rem; margin-block: -0.75rem; }
}

/* On paper a card that scrolls on screen is simply as long as its table, and nothing sticks. */
/* A sheet made for paper (the day's activity, a quote, the attendees) is a card that scrolls its own table on a phone, and the page does not slide. */
@media screen and (max-width: 48rem) { .day-sheet, .app-main > .quote-sheet, .attendees-print { overflow-x: auto; max-width: 100%; } } /* the staff's own print views; the customer's quote page keeps its look */
@media screen and (max-width: 48rem) { .day-sheet__table, .attendees-print table { min-width: 40rem; } } /* and its table keeps the columns it has on A4, so the card scrolls instead of the columns being squeezed to a few letters */
@media print {
  .table-wrap, .panel:has(> table), .panel:has(> form > table) { overflow: visible; background: none; }
  .table-wrap--sticky .col-select, .table-wrap--sticky .col-name { position: static; }
}

/* A heading of the page standing in a card (the refused and not-permitted pages) has the room under it that a card's heading has. */
.panel > h1 { margin-bottom: var(--space-4); }

/*
 * TAP TARGETS (the owner's decision, 18 October 2026): at 768 pixels and narrower every button, link and box a person presses is at least 40 pixels high and wide, in its own box
 * (an invisible area round a small one was not counted by anything that measures a target). The audit (npm run ui:audit, rule F) is what holds it. The frame's own (the small
 * buttons, the count in the bar, the back link, a tick box with its words) are 44 since 30 October 2026, the size of a control; a list's own links and boxes are still 40.
 */
@media (max-width: 48rem) {
  .task-badge { min-width: var(--control-height); height: var(--control-height); } /* 44 pixels (30 October 2026; it was 40) */
  .help-button { width: var(--control-height); height: var(--control-height); }
  .app-header__who .help-button::after, .app-header__who .task-badge::after { content: none; }
  .page-header__back a { display: inline-flex; align-items: center; min-height: var(--control-height); margin-block: -0.625rem; }
  .breadcrumbs a { display: inline-block; padding-block: 0.75rem; margin-block: -0.75rem; }
  /* A link on a line of its own (a name in a cell, a title, a count) is a box 40px high that takes no more room than its words: padding and the same again as a negative margin. */
  .app-main a:not([class]):not(:is(.tabs, .sub-nav, nav, th, .page-header__back, .breadcrumbs) a), .app-main :is(a.small, a.mono, .artist-head__change, .condition-badge-link, .section-card__link, .section-card__count) { display: inline-block; padding-block: 0.75rem; margin-block: -0.75rem; }
  .task-chip { padding-block: 0.5625rem; }
  .chip, .report-star__button { min-height: 2.5rem; }
  .report-star__button { width: 2.5rem; }
  /* A tick box or a radio is a box to press: its label is 44px high (30 October 2026; it was 40), and one with no label (a row's select box) is drawn 40px. */
  label:has(> input:is([type='checkbox'], [type='radio'])):not(.choice) { display: inline-flex; align-items: center; min-height: var(--control-height); }
  .choice label { min-height: var(--control-height); }
  td > input:is([type='checkbox'], [type='radio']), .task__select > input { width: 2.5rem; height: 2.5rem; margin: 0; flex: none; }
  .task--selectable { grid-template-columns: 2.5rem minmax(0, 1fr) auto; }
}
@media (max-width: 40rem) { .task--selectable { grid-template-columns: 2.5rem minmax(0, 1fr); } }

/* A heading that follows a block (a table, a form, a row of fields) has the room above it that a section has; it touched the block above on a dozen screens. */
:is(.panel, .app-main, form) > :is(.table-wrap, table, form, div, ul, ol, details, section) + :is(h2, h3) { margin-top: var(--space-6); }

/* A button in a row of fields stands at the right end of its line: an edit form's action is at the right (the owner's standard, 18 October 2026). Two together stand together. */
.row > :is(.btn, button),
.row > .field:not(:has(> label)):has(> :is(.btn, button)) { margin-left: auto; }
.row > :is(.btn, button, .field:not(:has(> label)):has(> :is(.btn, button))) + :is(.btn, button, .field:not(:has(> label)):has(> :is(.btn, button))) { margin-left: 0; }
/* The buttons of a table row (a user's Disable, Send reset ...) are a row of their own in the cell, from its left edge: they wrap there, and are not pushed to the right of the first line. Each keeps its words on one line, so the column is as wide as its widest button (a button's longest word was all the table asked for, and the button ran out of its cell). */
td > form.row > :is(.btn, button) { margin-left: 0; white-space: nowrap; }

/* ================================================================================================================================================================
   THE PHONE PASS, CONTENT (30 October 2026, branch staff-ui-collapse-phone; docs/Notes/staff-ui-collapse-audit-30-oct.md)
   One block of its own, so it merges beside the app frame's block (header, gutters, form bar, frame touch targets) and not through it. Three things:
   1. the fold (`src/ui/collapsible.tsx`, `public/section-fold.js`): a section under its own heading that opens when pressed;
   2. filter bars: the phone's "Filters" fold, the "More filters" fold, and the "Filtered by" line (`src/ui/filter-fold.tsx`);
   3. lists and long key-and-value tables on a phone: records, pagers and the Totals-style tables you can read.
   `test/section-fold.test.ts` and `test/filter-fold.test.ts` hold the rules that must not be lost.
   ================================================================================================================================================================ */

/* ---- 1. The fold ------------------------------------------------------------------------------------------------------------------------------------------------ */
/* A card whose heading is the thing you press: 44px tall, the heading and its one-line hint on a row, a chevron at the end. The content is the section's own, unchanged. */
.section-fold { padding: 0; }
.section-fold > .section-fold__summary {
  position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3);
  min-height: var(--control-height); padding: var(--space-3) calc(var(--space-6) + 1.75rem) var(--space-3) var(--space-6);
  list-style: none; cursor: pointer;
}
.section-fold > .section-fold__summary::-webkit-details-marker { display: none; }
.section-fold > .section-fold__summary::marker { content: ''; }
.section-fold > .section-fold__summary:hover { background: var(--wash); }
.section-fold > .section-fold__summary:focus-visible { outline-offset: -2px; }
.section-fold > .section-fold__summary::after {
  content: ''; position: absolute; right: var(--space-6); top: 50%; width: 0.55em; height: 0.55em;
  border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink);
  transform: translateY(-75%) rotate(45deg);
}
.section-fold[open] > .section-fold__summary::after { transform: translateY(-25%) rotate(-135deg); }
.section-fold__title { margin: 0; }
.section-fold__hint { color: var(--ink-muted); font-size: var(--text-small); min-width: 0; overflow-wrap: anywhere; }
.section-fold__needs { flex: none; }
.section-fold__body { padding: var(--space-2) var(--space-6) var(--space-6); min-width: 0; }
.section-fold__body > :first-child { margin-top: 0; }
.section-fold__body:has(> table, > form > table) { overflow-x: auto; max-width: 100%; }
/* A fold inside a card, or inside another fold, is a heading with a rule above it and no card of its own. */
.section-fold--bare { border: 0; border-top: 1px solid var(--line); margin-bottom: 0; background: transparent; }
.section-fold--bare > .section-fold__summary { padding-inline: 0; padding-right: 1.75rem; }
.section-fold--bare > .section-fold__summary::after { right: 0.25rem; }
.section-fold--bare > .section-fold__body { padding-inline: 0; }
@media (max-width: 40rem) {
  .section-fold > .section-fold__summary { padding: var(--space-3) calc(var(--space-4) + 1.5rem) var(--space-3) var(--space-4); }
  .section-fold > .section-fold__summary::after { right: var(--space-4); }
  .section-fold__body { padding: var(--space-1) var(--space-4) var(--space-4); }
  .section-fold__title { font-size: var(--text-h3); } /* 18px: a fold's heading is a button's width on a phone, and a 22px title wrapped */
}
/* On paper every fold is open and the chevron is gone (the script opens them before print; browsers that have ::details-content show the content with or without it). */
@media print {
  .section-fold > .section-fold__summary::after { display: none; }
  .section-fold > .section-fold__summary { min-height: 0; padding: 0 0 var(--space-2); }
  .section-fold__body { padding: 0; }
}
@media print { .section-fold::details-content { content-visibility: visible; } }

/* ---- 2. Filter bars ----------------------------------------------------------------------------------------------------------------------------------------------- */
/*
 * A filter bar shows the search box and the few boxes people use every time; the rest stand under "More filters" (`MoreFilters`, a <details> that opens by itself when one of its boxes is set).
 * On a phone the bar is shorter still: the search box, then one "Filters" control with the number set (`FilterToggle`, a summary-only <details> whose following boxes are hidden while it is
 * closed), then Clear and Search. No script: a closed <details> is native, and with no support for the rules below every box simply shows.
 */
.filters__more { grid-column: 1 / -1; border-top: 1px solid var(--line); padding-top: var(--space-1); }
.filters__more > summary, .filters__toggle > summary {
  position: relative; display: flex; align-items: center; gap: var(--space-2); min-height: var(--control-height);
  padding: 0 1.75rem 0 0; cursor: pointer; list-style: none;
  font-size: var(--text-small); letter-spacing: var(--tracking-label); text-transform: uppercase;
}
.filters__more > summary::-webkit-details-marker, .filters__toggle > summary::-webkit-details-marker { display: none; }
.filters__more > summary::marker, .filters__toggle > summary::marker { content: ''; }
.filters__more > summary::after, .filters__toggle > summary::after {
  content: ''; position: absolute; right: 0.25rem; top: 50%; width: 0.55em; height: 0.55em;
  border-right: 2px solid var(--ink); border-bottom: 2px solid var(--ink); transform: translateY(-75%) rotate(45deg);
}
.filters__more[open] > summary::after, .filters__toggle[open] > summary::after { transform: translateY(-25%) rotate(-135deg); }
.filters__more:not([open]) .filters__fewer, .filters__more[open] .filters__more-word { display: none; }
.filters__count {
  display: inline-flex; align-items: center; padding: 0.1rem var(--space-2); border: 1px solid var(--ink); background: var(--paper);
  font-size: var(--text-label); font-variant-numeric: tabular-nums; letter-spacing: normal; text-transform: none;
}
/* The extra boxes are a grid of their own with the panel's columns, so they line up as the boxes above them do. */
.filters__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--space-4) var(--space-3); align-items: start;
  padding-top: var(--space-3);
}
.filters__grid > .field { margin-bottom: 0; }
.filters__grid > .check-row { align-self: end; }
@media (min-width: 80rem) { .filters--aligned .filters__grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (min-width: 48rem) and (max-width: 79.99rem) { .filters--aligned .filters__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* Wide screens show every common box, so the phone control is not drawn there. */
.filters__toggle { display: none; }
@media (max-width: 40rem) {
  .filters__toggle { display: block; grid-column: 1 / -1; border: 1px solid var(--control-border); background: var(--paper); padding: 0 var(--space-3); }
  .filters__toggle > summary { padding-right: 1.5rem; }
  .filters__toggle > summary::after { right: 0.5rem; }
  .filters__toggle:not([open]) ~ :not(.filters__actions, .filters__search, .field__help, [type='hidden']) { display: none; }
  .filters__more { padding-top: 0; }
  .filters { padding: var(--space-3); gap: var(--space-3); }
}
/* What is narrowing the list, under the bar: one chip for each box that is set, each a link that takes that one off. */
.filters-active { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: calc(-1 * var(--space-3)) 0 var(--space-4); font-size: var(--text-small); }
.filters + .filters-active, .search-row--panel + .filters-active { margin-top: calc(-1 * var(--space-4)); }
.filters-active__label { color: var(--ink-muted); }
.filters-chip {
  display: inline-flex; align-items: center; gap: var(--space-2); min-height: 2rem; padding: 0 var(--space-3);
  border: 1px solid var(--control-border); background: var(--paper); color: var(--ink); text-decoration: none; overflow-wrap: anywhere;
}
.filters-chip:hover { background: var(--wash); }
.filters-chip__x { font-size: 1.1em; line-height: 1; color: var(--ink-muted); }
.filters-active__clear { padding: 0 var(--space-2); }
@media (max-width: 48rem) {
  .filters-chip { min-height: 2.5rem; }
  .filters-active__clear { display: inline-flex; align-items: center; min-height: 2.5rem; }
}
@media print { .filters-active { display: flex; margin-top: 0; } .filters-chip__x { display: none; } }

/* ---- 2b. The first line of a bar: the boxes and the buttons together (30 October 2026, the second pass) ----------------------------------------------------------------- */
/*
 * David: "the Clear and Search buttons [should] go on the same row as the boxes ... and the More Filters dropdown could also take less space". `FilterRow` (src/ui/filter-fold.tsx) is one wrapping flex
 * line holding the search box, the common boxes and then `FilterActions`: Clear then Search, at the right end of the very line the boxes stand on, wrapping together (and staying at the right) when the
 * screen is too narrow for them all. A row of tick boxes (`.filters__ticks`) is the next slim line under it, then `MoreFilters`, the row's last child; on a phone the ticks stand right under the search box
 * and the buttons come after More filters. Every filter bar of the staff screens is written this way (3 November 2026); a note that says what a box means stands under the row, across the panel (`.field__help`).
 * Nothing here sets a colour: tokens only.
 */
.filters:has(> .filters__row) { gap: var(--space-2) var(--space-3); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }
.filters:has(> .filters__row) + .filters-active { margin-top: calc(-1 * var(--space-2)); }
.filters__row { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-2) var(--space-3); min-width: 0; }
.filters__row > .field { flex: 1 1 9rem; min-width: 0; margin: 0; }
.filters__row > .filters__search { flex: 2 1 15rem; min-width: 0; }
.filters__row > .filters__ticks { flex: 1 1 100%; order: 9; }
.filters__row > .filters__actions { grid-column: auto; justify-self: auto; flex: none; flex-wrap: nowrap; margin-left: auto; }
/* The slim ones: on a mouse the tick boxes and the closed More filters line are a line of text, not a 44px bar (a touch screen keeps its 44px, below). */
@media (min-width: 40.01rem) and (pointer: fine) {
  .filters__row > .filters__ticks > label { min-height: 1.75rem; }
  .filters__more > summary { min-height: 2rem; }
}
/* In a bar with a row, More filters is the row's last item: a small toggle under the line the buttons are on, with no rule and no padding of its own, and the extra boxes open as a tight grid of several to a line. */
.filters__row > .filters__more { flex: 1 1 100%; order: 10; border-top: 0; padding-top: 0; }
.filters__row > .filters__more > summary { width: max-content; max-width: 100%; padding-right: 1.5rem; }
.filters__row .filters__grid { grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); gap: var(--space-2) var(--space-3); padding-top: var(--space-2); }
@media (max-width: 40rem) {
  .filters:has(> .filters__row) { padding: var(--space-3); gap: var(--space-2); }
  .filters__row { gap: var(--space-2); }
  .filters__row > .filters__search, .filters__row > .filters__ticks, .filters__row > .field { flex: 1 1 100%; }
  .filters__row > .filters__ticks { order: 0; display: grid; grid-template-columns: auto 1fr; gap: 0 var(--space-3); }
  /* Closed: Filters, Clear and Search share one line (the boxes and More filters are not drawn). Open: Filters has a line, the boxes a line each (a name can be long), the extra boxes under More filters two to a line, and the buttons the last line, sharing it. */
  .filters__row > .filters__toggle { flex: 1 1 8rem; }
  .filters__row > .filters__toggle[open] { flex-basis: 100%; }
  .filters__row > .filters__actions { flex: none; margin-left: 0; order: 20; }
  .filters__row > .filters__actions > .btn { flex: none; }
  .filters__row > .filters__toggle[open] ~ .filters__actions { flex: 1 1 100%; }
  .filters__row > .filters__toggle[open] ~ .filters__actions > .btn { flex: 1 1 0; }
  /* A bar with no Filters control (every box is always drawn): Clear and Search are the last line and share it, as the stacked bars' always did on a phone. */
  .filters__row:not(:has(> .filters__toggle)) > .filters__actions { flex: 1 1 100%; }
  .filters__row:not(:has(> .filters__toggle)) > .filters__actions > .btn { flex: 1 1 0; }
  /* A box whose choices are long words (a Photographs, Came in, Framing or Contact details box) has a line to itself. */
  .filters__row .filters__grid > .filters__long { grid-column: 1 / -1; }
  .filters__row .filters__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding-top: var(--space-2); }
}

/* A small button keeps its words on one line (a long one, 'Ask the artist for more information', was 313px wide and slid the whole artwork page sideways on a phone): on a phone it may wrap, and is never wider than its card. */
@media (max-width: 40rem) { .btn { max-width: 100%; } .btn--small { white-space: normal; text-align: center; padding-block: var(--space-1); } }

/* The tiles of a landing page (Settings, Gallery Management, Reports) are a list on a phone: closer together, with the explanation a size smaller, so twenty of them are not five screens. */
@media (max-width: 40rem) {
  .section-grid { gap: var(--space-2); margin-bottom: var(--space-6); }
  .section-card { padding: var(--space-3) var(--space-4) var(--space-4); }
  .section-card__blurb { font-size: var(--text-small); line-height: 1.4; }
}

/* ---- 3. Lists and tables on a phone -------------------------------------------------------------------------------------------------------------------------------- */
/* A record on a phone has no Image cell: the rule that hides it (.table--collapse td[data-label='Image']) lost to the rule that makes a cell holding a block a block (which counts a div's specificity), so an empty picture box and the word IMAGE stood at the head of every artist and artwork. */
@media (max-width: 60rem) { .table--collapse tbody td[data-label='Image'] { display: none; } }
/* Previous and Next are buttons a thumb can press: the whole width, 44px, with the "11-20 of 53" line above them. */
@media (max-width: 48rem) {
  .pager .btn, .pagination .btn { min-height: var(--control-height); padding-inline: var(--space-5); }
  .pager > .muted:first-child { flex: 1 1 100%; }
  .pager > .btn { flex: 1 1 8rem; }
}
/*
 * A key and value table (.table--detail: Totals, a customer's details, a work's facts) on a phone. It was stacked, each key over its value, so six short rows were 566px tall. The key and its
 * value stand on one line now, the key in about a third; a long value wraps in its own column. A row with one cell takes the whole width.
 */
@media (max-width: 40rem) {
  table.table--detail tr { display: grid; grid-template-columns: minmax(6rem, 36%) minmax(0, 1fr); }
  table.table--detail th, table.table--detail td { padding: var(--space-2) 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
  table.table--detail td { padding-left: var(--space-3); }
  table.table--detail td.money, table.table--detail td.numeric { text-align: right; }
  table.table--detail tr > :only-child { grid-column: 1 / -1; }
  table.table--detail tbody tr:last-child th, table.table--detail tbody tr:last-child td { border-bottom: 0; }
}

/*av:begin same in app.css and till.css (staff-pictures-rules.test.ts)*/
.avatar{--s:2rem;position:relative;display:inline-grid;place-items:center;flex:none;width:var(--s);height:var(--s);overflow:hidden;background:var(--sage);color:var(--ink);font-size:calc(var(--s)*.4);font-weight:600;line-height:1}
.avatar--xs{--s:1.5rem}.avatar--md{--s:3rem}.avatar--lg{--s:6rem}.avatar--xl{--s:10rem}
.avatar--art{background:var(--sage-wash)}
.avatar>img,.avatar>svg{display:block;width:100%;height:100%;object-fit:cover}
.avatar>img::after{content:attr(data-initials);position:absolute;inset:0;display:grid;place-items:center;background:var(--sage)}
.person{display:inline-flex;align-items:center;gap:.4rem;min-width:0}
/*av:end*/
@media (forced-colors:active){.avatar{border:1px solid CanvasText}}

/* Where the picture sits in the application (not in the till): beside the title, beside a panel's heading, and beside a name in the bar. */
.page-header__lead { display: flex; align-items: center; gap: var(--space-4); min-width: 0; }
.page-header__lead h1 { margin-top: 0; }
.with-avatar { display: flex; align-items: center; gap: var(--space-3); }
.person { vertical-align: middle; }

/* The artist portal's welcome message (Settings, Artist portal, Welcome message; 21 October 2026): a sender's photograph in a circle, and the preview of the message as an artist reads it. */
.welcome-photo { display: inline-block; width: 4rem; height: 4rem; border-radius: 50%; object-fit: cover; border: 1px solid var(--line); vertical-align: middle; background: var(--sage-wash); }
.welcome-photo--blank { display: inline-grid; place-items: center; width: 3rem; height: 3rem; font-weight: 600; }
.welcome-preview { border: 1px solid var(--line); border-left: 4px solid var(--sage); background: var(--sage-wash); padding: var(--space-5); }
.welcome-preview > :first-child { margin-top: 0; }
.welcome-preview__people { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-6); margin: var(--space-4) 0 0; padding: 0; }
.welcome-preview__people li { display: flex; align-items: center; gap: var(--space-3); }
.welcome-preview .welcome-photo { width: 3rem; height: 3rem; }

/* The gallery's page for the photos an artist sent (the artist portal, part 2B, 22 October 2026): a photo is a row like a picture on the Images panel, with its decline below. */
.image-row__decline { margin-top: var(--space-2); padding-top: var(--space-3); border-top: 1px solid var(--line); }
.image-row__decline .row { align-items: flex-end; }

/* Messages from artists (the artist portal, part 3B, 22 October 2026). A note among staff and a reply the artist can read are drawn differently on purpose: a dashed amber frame and the words "Internal note,
   staff only", against a solid green frame and the words "Reply, the artist can read this". The composer takes the same two looks, so the choice shows before anything is posted. */
.thread { display: grid; gap: var(--space-3); margin: var(--space-4) 0; }
.thread-msg { border: 1px solid var(--line); border-left-width: 4px; background: var(--paper); padding: var(--space-3) var(--space-4); }
.thread-msg--artist { border-left-color: var(--sage); }
.thread-msg--internal { border-style: dashed; border-left-style: solid; border-left-color: var(--warn); background: var(--warn-wash); }
.thread-msg--reply { border-left-color: var(--ok); background: var(--ok-wash); }
.thread-msg__head { margin: 0 0 var(--space-2); }
.thread-msg__body { overflow-wrap: anywhere; }
.thread-tag { display: inline-block; font-size: var(--text-small); font-weight: 600; padding: 0 var(--space-2); border: 1px solid currentColor; }
.thread-tag--internal { color: var(--warn); }
.thread-tag--reply { color: var(--ok); }
.thread-tag--artist { color: var(--ink); }
.thread-files { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: grid; gap: var(--space-2); }
.thread-files__img { display: block; max-width: 10rem; height: auto; border: 1px solid var(--line); background: var(--wash); margin-bottom: var(--space-1); }
.composer { border: 2px dashed var(--warn); background: var(--warn-wash); padding: var(--space-4); }
.composer__choice { border: 0; padding: 0; margin: 0 0 var(--space-4); }
.composer__choice legend { font-weight: 600; padding: 0; margin-bottom: var(--space-2); }
.composer__option { display: flex; gap: var(--space-2); align-items: flex-start; padding: var(--space-2) 0; }
.composer__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-3); }
.composer__hint--internal, .composer__hint--reply { display: none; }
@supports selector(:has(*)) {
  .composer__hint--neutral { display: none; }
  .composer__hint--internal { display: inline; }
  .composer:has(input[value='reply']:checked) { border-style: solid; border-color: var(--ok); background: var(--ok-wash); }
  .composer:has(input[value='reply']:checked) .composer__hint--internal { display: none; }
  .composer:has(input[value='reply']:checked) .composer__hint--reply { display: inline; }
}

/* The Users list (25 October 2026). A person's row is their facts and one quiet Manage button; the button opens a panel of labelled groups (picture and colour, sign-in and security,
   access and role) divided by a rule, with the one that does damage last and its rule and heading in the danger colour. The cell takes the room for the panel while it is open
   (the rule for a cell with a disclosure in it, above, asks for more than the panel needs: its buttons wrap), and the person's facts stay level with the button. */
.table--collapse td:has(> .user-manage[open]) { min-width: 21rem; }
.table--collapse tbody tr:has(.user-manage[open]) > td { vertical-align: top; }
@media (max-width: 60rem) { .table--collapse td:has(> .user-manage[open]) { min-width: 0; } }
.user-manage > summary { list-style: none; display: inline-flex; cursor: pointer; }
.user-manage > summary::-webkit-details-marker { display: none; }
.user-manage[open] > summary { margin-bottom: var(--space-3); }
/* Edit (2 November 2026) is a link beside Manage in the person's last cell: the disclosure sits next to it, and its panel opens under its own button. */
.table--collapse td.stacked > .btn + .user-manage { display: inline-block; vertical-align: top; margin-left: var(--space-2); }
.user-manage__panel { border: 1px solid var(--line); background: var(--paper); padding: 0 var(--space-4); text-align: left; }
.user-manage__group { padding: var(--space-3) 0; border-top: 1px solid var(--line); }
.user-manage__panel > form:first-child > .user-manage__group:first-of-type { border-top: 0; }
.user-manage__group h3 { margin: 0 0 var(--space-2); font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-muted); font-weight: 500; }
.user-manage__group p { margin: var(--space-2) 0 0; }
.user-manage__group .cluster { gap: var(--space-2); }
.user-manage__group .row > select { flex: 1 1 8rem; width: auto; min-width: 0; } /* the role box takes what the button leaves, not the whole line */
.user-manage__group--danger { border-top-color: var(--danger); }
.user-manage__group--danger h3 { color: var(--danger); }

/* Settings, Xero (26 October 2026): one table, each thing Flag books once, with where it stands. A group is a plain heading row; the "?" opens the long explanation so a row stays one line;
   a row filled by "Fill from my chart" is highlighted until it is saved; the folded Advanced and Connection panels are quiet. */
.xero-map__group th { background: var(--wash); font-size: var(--text-label); letter-spacing: var(--tracking-label); text-transform: uppercase; font-weight: 600; color: var(--ink); padding-top: var(--space-4); }
.xero-map__blurb { display: block; text-transform: none; letter-spacing: normal; font-weight: 400; margin-top: var(--space-1); }
.xero-map td { vertical-align: top; }
.xero-map td:first-child { width: 30%; }
.xero-map td:nth-child(2) { width: 27%; min-width: 14rem; }
#where [hidden] { display: none !important; } /* the fill note, the banner and Undo set their own display, which the attribute alone would not beat */
.xero-map input[type="text"] { width: 8.5rem; }
.xero-help { display: inline-block; margin-left: var(--space-2); vertical-align: baseline; }
.xero-help > summary { list-style: none; display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; border: 1px solid var(--ink-muted); border-radius: 50%; color: var(--ink-muted); font-size: 0.75rem; cursor: pointer; }
.xero-help > summary::-webkit-details-marker { display: none; }
.xero-help[open] > summary { color: var(--ink); border-color: var(--ink); }
.xero-help__body { display: block; margin-top: var(--space-2); max-width: 36rem; }
.xero-help__body p { margin: 0 0 var(--space-1); }
.xero-status { display: inline-flex; gap: var(--space-2); align-items: baseline; }
.xero-status__mark { font-weight: 600; }
.xero-status--ok .xero-status__mark { color: var(--ok); }
.xero-status--unset, .xero-status--differs, .xero-status--control, .xero-status--unknown, .xero-status--inactive { color: var(--danger); }
.xero-status--off, .xero-status--unused, .xero-status--optional { color: var(--ink-muted); }
tr.is-filled td { background: color-mix(in srgb, var(--warn) 14%, transparent); }
tr.is-filled .xero-status--filled { color: var(--ink); font-weight: 500; }
.xero-advanced > summary, #connection > summary { cursor: pointer; }
.xero-advanced[open] > summary, #connection[open] > summary { margin-bottom: var(--space-4); }
.xero-advanced > h3, .xero-advanced__inner { margin-top: var(--space-6); }
.xero-advanced__inner > summary { cursor: pointer; }
.xero-advanced__inner[open] > summary { margin-bottom: var(--space-4); }
.xero-details__group th { font-size: var(--text-small); color: var(--ink-muted); font-weight: 500; text-align: left; padding-bottom: 0; }
.xero-details td:first-child { min-width: 14rem; }
@media (max-width: 60rem) { .xero-details__group { display: none; } }
@media (max-width: 60rem) {
  /* A row is a card on a phone: every part stands on its own line with its label above, and the wash is the card's, not each cell's. */
  .xero-map td[data-label="Where it stands"] { display: block; text-align: left; }
  .xero-map td[data-label="Where it stands"]::before { display: block; margin-bottom: var(--space-1); }
  .xero-map tr.is-flagged td, .xero-map tr.is-filled td { background: transparent; }
  .xero-map tr.is-flagged { background: color-mix(in srgb, var(--danger) 6%, transparent); }
  .xero-map tr.is-filled { background: color-mix(in srgb, var(--warn) 14%, transparent); }
}

/*
 * The Sell screen: the Finish panel and the event tag (David, 1 November 2026; `src/ui/sell-finish.tsx`).
 *
 * Finish is one row of buttons with the key action at the right end: Cancel sale (quiet, pinned at the left), then Save incomplete sale, Save as quote and Email invoice, then Complete sale
 * last. On a phone the row is one column at the full width in the same order, so Complete sale is the foot (the house rule for a form's buttons). The hold option above it is one tick and
 * its length, read by both Save buttons; the two forms that stand empty-handed (`finish-keep` with no works, `finish-complete` with no emails to ask about) draw nothing.
 */
.sell-finish #finish-keep:empty, .sell-finish #finish-complete:empty { display: none; }
.sell-finish #finish-complete > .email-prompt { margin-top: 0; }
.sell-hold { margin: 0 0 var(--space-4); }
.sell-hold__now { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-4); margin-bottom: var(--space-4); padding: var(--space-3) var(--space-4); background: var(--wash); border: 1px solid var(--line); }
.sell-hold__now > p { flex: 1 1 18rem; }
.sell-hold__now > form { margin: 0; }
.sell-hold__choice { gap: var(--space-2) var(--space-4); }
.sell-hold__length { display: inline-flex; align-items: center; gap: var(--space-2); }
.sell-hold__length select { width: auto; min-width: 7rem; }
.sell-hold .field__help { margin-top: var(--space-2); }
.sell-finish__bar { align-items: center; margin-top: var(--space-4); }
.sell-finish__bar > form { display: flex; margin: 0; }
.sell-finish__bar > .sell-finish__cancel { margin-right: auto; }
@media (max-width: 40rem) {
  .sell-finish__bar { flex-direction: column; flex-wrap: nowrap; align-items: stretch; }
  .sell-finish__bar > .sell-finish__cancel { margin-right: 0; }
  .sell-finish__bar .btn { width: 100%; }
}
/* The sentence shown when a Finish button was pressed with a note not saved (2 November 2026; `public/form-guard.js`, `SaveNotesFirst`): in the Notes panel above its Save button, and under the Finish buttons. Hidden until the script shows it. */
.save-first { margin: var(--space-3) 0; padding: var(--space-3) var(--space-4); border: 1px solid var(--line); border-left: 3px solid var(--warn); background: var(--warn-wash); color: var(--ink); font-size: var(--text-small); font-weight: 600; }
.save-first[hidden] { display: none; }
/* The event the sale is tagged with: one quiet line under the header, with its way to change it. */
.sell-tag { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1) var(--space-3); margin: 0 0 var(--space-4); }

/* Markdown alignment (5 November 2026): a picture lined up by {align=...} after it, and a block of text between :::left, :::center or :::right and ::: lines.
   The renderer writes only these three class names (shared/markdown.ts), because a Content-Security-Policy that allows no inline style (the quote page) leaves
   a class working where a style would not. Pictures are display:block here, so a side margin of auto is what moves one. */
.md-align-left { text-align: left; }
.md-align-center { text-align: center; }
.md-align-right { text-align: right; }
img.md-align-left, .md-align-left img { display: block; margin-left: 0; margin-right: auto; }
img.md-align-center, .md-align-center img { display: block; margin-left: auto; margin-right: auto; }
img.md-align-right, .md-align-right img { display: block; margin-left: auto; margin-right: 0; }

/* A row of pictures (7 November 2026): :::row ... ::: lines, which shared/markdown.ts draws as a div of two class names (md-row and md-row-2, -3 or -4) round two to four pictures.
   A grid of equal columns, so the pictures sit side by side at the same width, top-aligned, whatever their shapes. Two stay side by side at every width, a phone's included;
   three or four wrap to two a line on a narrow screen. Class names only, no style (the quote page allows none). The pictures have no margin of their own here: the row has it. */
.md-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); align-items: start; margin: var(--space-6) 0; }
.md-row-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.md-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.md-row-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Unequal widths and vertical place (11 November 2026): a row of two whose pictures state shares ({width=20%} and {width=80%}) has a third class, md-row-split-N, N being the first picture's share
   brought to the nearest of this closed list (the second has the rest; 50 is the plain equal row and has no class). Class names only, no style (the quote page allows none), so the numbers are
   fixed here and shared/markdown.ts only ever writes one of these. They follow .md-row-2 and are the same weight, so they win. A row opened :::row middle has md-row-middle: the shorter
   pictures centred up and down against the tallest (the default, start, is the top). */
.md-row-split-10 { grid-template-columns: minmax(0, 10fr) minmax(0, 90fr); }
.md-row-split-15 { grid-template-columns: minmax(0, 15fr) minmax(0, 85fr); }
.md-row-split-20 { grid-template-columns: minmax(0, 20fr) minmax(0, 80fr); }
.md-row-split-25 { grid-template-columns: minmax(0, 25fr) minmax(0, 75fr); }
.md-row-split-30 { grid-template-columns: minmax(0, 30fr) minmax(0, 70fr); }
.md-row-split-33 { grid-template-columns: minmax(0, 33fr) minmax(0, 67fr); }
.md-row-split-40 { grid-template-columns: minmax(0, 40fr) minmax(0, 60fr); }
.md-row-split-60 { grid-template-columns: minmax(0, 60fr) minmax(0, 40fr); }
.md-row-split-67 { grid-template-columns: minmax(0, 67fr) minmax(0, 33fr); }
.md-row-split-70 { grid-template-columns: minmax(0, 70fr) minmax(0, 30fr); }
.md-row-split-75 { grid-template-columns: minmax(0, 75fr) minmax(0, 25fr); }
.md-row-split-80 { grid-template-columns: minmax(0, 80fr) minmax(0, 20fr); }
.md-row-split-85 { grid-template-columns: minmax(0, 85fr) minmax(0, 15fr); }
.md-row-split-90 { grid-template-columns: minmax(0, 90fr) minmax(0, 10fr); }
.md-row-middle { align-items: center; }
.md-row img, .prose .md-row img { display: block; width: 100%; min-width: 0; height: auto; margin: 0; }
.prose .md-row { margin-top: var(--space-4); margin-bottom: var(--space-4); }
@media (max-width: 40rem) {
  .md-row-3, .md-row-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ------------- Rules about the words and colours of a picture (Stage 23, 8 November 2026) --- */
/* One box for each kind of tag: its choices, a box to find a word, and the approved words as ticks in a scrolling list. A word the find box hides keeps its tick. */
.facet-rule { margin-bottom: var(--space-6); }
.word-list { list-style: none; margin: 0 0 var(--space-3); padding: var(--space-2) var(--space-3); max-height: 14rem; overflow: auto; border: 1px solid var(--line); display: grid; gap: 0 var(--space-4); grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); }
.word-list li[hidden] { display: none; }
.word-list .check { display: flex; align-items: flex-start; gap: var(--space-2); margin: 0; padding: var(--space-1) 0; }
.word-list .check > input { margin-top: 0.25rem; flex: none; }
.word-list__stray .check span { color: var(--danger); }
.word-list--colours { max-height: none; overflow: visible; }
.swatch { display: inline-block; width: 1rem; height: 1rem; margin-top: 0.2rem; border: 1px solid var(--line); border-radius: 50%; flex: none; }
/* The pictures of the works the preview finds: the Artworks gallery view's tiles, smaller. */
.preview-grid { grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); gap: var(--space-4) var(--space-3); }
.preview-grid .gallery-card__title { font-size: var(--text-small); }
.notice ul { margin: var(--space-2) 0 0; padding-left: var(--space-5); }

/* ------------- Prospects (Stage 25, piece 4a, 10 November 2026) --- */
/* A customer contacted lately is greyed, not hidden: the page says how long ago, and a tick takes them out. It comes back to full strength under the pointer or the keyboard, so nothing in the row is hard to read. */
.prospect-row--cooling { opacity: 0.6; }
.prospect-row--cooling:hover, .prospect-row--cooling:focus-within { opacity: 1; }
/* The score is a plain figure; Strong sits beside it. The consent badge is the one thing in a row that must not be missed, so it is a little heavier than a badge elsewhere. */
.prospect-score { font-size: var(--text-body); margin-right: var(--space-2); }
.prospect-badge { font-weight: 600; white-space: normal; }
/* The reasons and the things to do are fold-away parts of a row: a details with a plain summary. */
.prospect-why > summary { cursor: pointer; }
.prospect-why__list { margin: var(--space-2) 0 0; padding-left: var(--space-5); }
.prospect-thumbs { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.prospect-thumbs img { width: 3.5rem; height: 3.5rem; object-fit: cover; background: var(--wash); border: 1px solid var(--line); }
.prospect-actions > summary { cursor: pointer; display: inline-flex; list-style: none; }
.prospect-actions > summary::-webkit-details-marker { display: none; }
.prospect-actions__body { margin-top: var(--space-3); min-width: 14rem; }
.prospect-form { margin: var(--space-4) 0 0; padding-top: var(--space-4); border-top: 1px solid var(--line); }
.prospect-form__title { margin: 0 0 var(--space-2); }

/* ------------- Prospects (Stage 25, piece 4b, 10 November 2026): the call sheet --- */
/* A page for phoning down a list on paper: each person with their number big enough to read across a desk, the badge in words, and blank boxes to write Called, Spoke to and a Note in. It is a page of the
   application and never a file: the browser's own Print makes the paper. Nothing of a score or a reason is ever on it. */
.call-sheet { max-width: 60rem; margin: 0 auto; }
.call-sheet__head { margin-bottom: var(--space-4); }
.call-sheet__list, .call-sheet__works { list-style: none; margin: 0; padding: 0; }
.call-sheet__works li { padding: var(--space-3) 0; border-top: 1px solid var(--line); break-inside: avoid; }
.call-sheet__entry { display: grid; grid-template-columns: minmax(12rem, 1fr) 2fr; gap: var(--space-4); padding: var(--space-4) 0; border-top: 1px solid var(--line); break-inside: avoid; }
.call-sheet__name { font-size: var(--text-lede, 1.1rem); }
.call-sheet__phone { font-size: 1.35rem; font-weight: 600; font-variant-numeric: tabular-nums; margin: var(--space-1) 0; }
.call-sheet__badge { font-size: var(--text-small); }
.call-sheet__contacted { font-size: var(--text-small); }
.call-sheet__subhead { margin: var(--space-5) 0 var(--space-2); font-size: var(--text-body); }
.call-sheet__boxes { display: grid; grid-template-columns: 1fr 2fr; gap: var(--space-2); align-content: start; }
.call-sheet__box { border: 1px solid var(--ink); min-height: 2.5rem; padding: 2px var(--space-2); }
.call-sheet__box--note { grid-column: 1 / -1; min-height: 4.5rem; }
.call-sheet__label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-muted); }
.call-sheet__foot { margin-top: var(--space-5); }
@media (max-width: 40rem) { .call-sheet__entry { grid-template-columns: 1fr; } }
@media print {
  @page { size: A4 portrait; }
  .call-sheet { max-width: none; }
  .call-sheet__entry, .call-sheet__works li { border-top-color: #000; }
  .call-sheet__box { border-color: #000; }
  .call-sheet__label, .call-sheet__contacted { color: #000; }
  .call-sheet__work .artwork-line__thumb { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
}
