/*
 * 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-6);
}

/* 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-6); flex: 1; flex-wrap: wrap; }
.app-nav a {
  text-decoration: none;
  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: var(--space-3);
  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);
}

/* Phone: the nav collapses into a details/summary disclosure. */
@media (max-width: 60rem) {
  .app-header__bar { flex-wrap: wrap; gap: var(--space-3); }
  .app-nav { gap: var(--space-4); order: 3; width: 100%; }
  .app-main { padding: var(--space-6) var(--space-4) var(--space-12); }
}
/* On a phone the navigation stays visible as a compact wrapping row. A
   hidden menu behind a toggle would need JavaScript to open, and a member of
   staff on the shop floor should never be one broken script away from having
   no way to reach the artworks list. */
@media (max-width: 40rem) {
  .app-header__bar { padding: var(--space-3) var(--space-4); }
  .app-nav { gap: var(--space-3); row-gap: var(--space-1); }
  .app-nav a { padding: var(--space-1) 0; }
  .app-header__who { font-size: var(--text-label); gap: var(--space-2); }
}

/* ------------------------------------------------------- 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__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.page-header .lede { margin: var(--space-1) 0 0; }

.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); }

/* -------------------------------------------------------------- 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); }

.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(--line);
  border-radius: var(--radius);
}
textarea { min-height: 6rem; resize: vertical; line-height: var(--leading-body); }
input:hover, select:hover, textarea:hover { border-color: var(--ink-muted); }
input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--ink); }

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); }
.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(--line); 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) 0; border: 0; width: 100%;
}

.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: var(--space-4); }
.form-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-6); }

/*
 * 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).
 */
.form-action-bar {
  position: sticky; bottom: 0; z-index: 20;
  display: flex; flex-wrap: wrap; align-items: center; 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 { order: 1; display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.form-action-bar__status { order: 2; margin-left: 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 share the width, each a full-height thumb target. */
@media (max-width: 40rem) {
  .form-action-bar { flex-direction: column; align-items: stretch; gap: var(--space-2); }
  .form-action-bar__status { order: 0; margin-left: 0; }
  .form-action-bar__buttons { order: 1; flex-wrap: nowrap; }
  .form-action-bar__buttons > .btn { flex: 1 1 0; min-width: 0; }
}
@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:has(> .btn:only-child), .row > .field:has(> button:only-child) { flex: 0 0 auto; }

/* 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; }

/* 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;
}

/*
 * 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 + :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 + :is(input, select, textarea))
  > .field:has(> :is(.btn, button):only-child) {
  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; }

/*
 * 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 three controls beside it. That was the artworks list looking wrong at
 * the top without it being obvious which part was wrong.
 */
.filters .field:has(> :is(.btn, button):only-child) { margin-top: var(--field-label-height); }
.filters__search { grid-column: span 2; min-width: 16rem; }
@media (max-width: 40rem) { .filters__search { grid-column: span 1; } }

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

.table-wrap { overflow-x: auto; border: 1px solid var(--line); background: var(--paper); }
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); }

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); }

.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;
}
/* 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) {
  .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); }

/* 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); }

/* 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; }

/* 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); }

/* ------------------------------------------------------- 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);
}
.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] { display: none !important; }
.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; justify-content: space-between; align-items: center; gap: 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); }
@media (max-width: 40rem) {
  .artist-head { flex-direction: column; }
  .artist-head__photo { width: 100%; max-width: 14rem; }
}

/* 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 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); }

/* ----------------------------------------------------------------- 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 bar, and a disclosure on a narrow screen.

   One piece of markup for both, with no script: `<details open>` renders as a
   row of links on a desk because the summary is hidden and the list is forced
   visible, and as a tap-to-open menu in a hand because neither rule applies.
   Ten links across a tablet wrap into three rows and push the page itself
   below the fold, which is the whole reason this collapses. */
.sub-nav {
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--space-6);
}
/* Closed in the markup, forced open here. A `<details>` hides its own
   contents when closed, and `display` on the child overrides that — which is
   the only direction this works in: CSS can reveal a closed disclosure and
   cannot close an open one. */
.sub-nav__summary { display: none; }
.sub-nav > nav { display: block; }
.sub-nav__links {
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-5);
  list-style: none; margin: 0; padding: 0 0 var(--space-3);
}
.sub-nav__links a {
  display: inline-block; padding: var(--space-1) 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: 60rem) {
  /* And closed again in the hand. The rule above reveals a closed
     `<details>` at every width, so the narrow case has to put it back —
     measured rather than assumed, because it looked collapsed and was not. */
  .sub-nav:not([open]) > nav { display: none; }
  .sub-nav__summary {
    display: block; cursor: pointer;
    padding: var(--space-3) 0;
    font-size: var(--text-small);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }
  .sub-nav__summary::marker { color: var(--ink-muted); }
  .sub-nav__links {
    flex-direction: column; gap: 0;
    padding-bottom: var(--space-4);
  }
  .sub-nav__links a { padding: var(--space-2) 0; }
}

.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; }
.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); }

/*
 * The artwork page's action bar: three equal panels, each about one thing a
 * customer might want.
 *
 * `auto-fit` rather than a fixed three, so a narrow screen gets one column
 * and a tablet gets two without a media query — and `1fr` on the rows so the
 * three stay the same height however long their refusals are. A row of
 * buttons had nowhere to say why one of them was unavailable; a panel does,
 * and unequal heights would make the one with something to say look broken.
 */
/* 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; }

.actions-grid {
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  margin-bottom: var(--space-6);
}
/*
 * Equal heights come from the grid stretching them, and the content stays at
 * the top of each.
 *
 * The first version pushed each panel's form to the foot with `margin-top:
 * auto`, meaning to line the three buttons up. What it actually did was push
 * the *whole form* down by however much taller the tallest panel was — so
 * Sell, which is one picker and a button, rendered as a heading, a sentence
 * and then nothing, with its controls somewhere below the fold. It read as a
 * broken panel rather than a tidy one. Found by looking at it.
 */
.actions-grid > .action-panel { margin-bottom: 0; }

/*
 * 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; }
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; }
.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); }

/* ------------------------------------------------------------ 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; }

/* ---------------------------------------------------------------- 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);
}
.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); }
.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);
}

.task {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  justify-content: space-between;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}

.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;
}

.task__actions { flex: 0 0 auto; }
.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 { border-left: 3px solid var(--danger); padding-left: var(--space-3); }
.task--high:not(.task--overdue) { border-left: 3px solid var(--warn); padding-left: var(--space-3); }

.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 on a task row, for the bulk bar, and the bar's own controls. */
.task__select { flex: 0 0 auto; margin-top: var(--space-1); }
.bulk-bar__actions { align-items: flex-end; flex-wrap: wrap; }


.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 row rather than squeezing the title into
 * a column two words wide. */
@media (max-width: 40rem) {
  .task { flex-direction: column; align-items: stretch; }
  .task__actions { justify-content: flex-start; }
}

/*
 * 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 { flex-direction: column; align-items: stretch; }
.task-board .task__actions { 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; }

/* 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: hidden; text-overflow: ellipsis; }
.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; }
/* 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; align-items: center; gap: var(--space-3); padding: var(--space-2); border: 1px solid var(--line); background: var(--paper); }
.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; } }

/* A task about a work shows the work, under the task's own line. */
.task__work { margin-top: var(--space-2); }

/* 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; }

/* 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-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__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); }
.roster-filter select { min-height: 2rem; width: auto; font-size: var(--text-small); }

/* 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); 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: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; }

.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); }
.roster-day-forms input[type='text'] { flex: 1 1 12rem; }

/* 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 var(--space-2); 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); }

/* 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 phone has no room for seven columns: each day is a row, and the days from the next or the last month are left out. */
@media (max-width: 40rem) {
  .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-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); }
@media (max-width: 64rem) { .roster-publish-bar__buttons .btn--quiet { display: none; } }
@media (max-width: 40rem) {
  html:has(.roster-publish-bar) { --roster-publish-bar-h: 4.25rem; }
  .roster-publish-bar__detail { display: none; }
  .roster-publish-bar__status .badge { white-space: normal; text-align: left; line-height: 1.25; letter-spacing: 0.03em; }
  .roster-publish-bar__buttons .btn { padding: 0 var(--space-3); letter-spacing: 0.03em; }
}
@media print { .roster-publish-bar { 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); }
.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; }

/* 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); }
.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); }
