/* Vesopa Back Office — dark rail, light canvas, generous whitespace. */

:root {
  /* THE RAIL FOLLOWS THE THEME.
   *
   * These were #111111 in :root and overridden nowhere, so Day mode painted a
   * light page against a black sidebar and a black top bar — which is not a
   * light theme, it is a dark theme with a light hole in the middle. Reported
   * as "day mode should actually be day rather than black".
   *
   * The dark values move to the two dark blocks further down, unchanged. */
  --rail: #ffffff;
  --rail-text: #4b4753;
  --rail-active: #17141c;
  --rail-line: rgba(20, 18, 26, .10);
  --rail-hover: rgba(20, 18, 26, .05);
  --rail-group: #8a8494;
  --rail-scroll: rgba(20, 18, 26, .18);
  --bg: #f5f4f7;
  --card: #ffffff;
  --line: #e6e3ea;
  --text: #17141c;
  --muted: #78717f;

  /* Taken from the Vesopa mark: the lime tail on the "V". */
  --brand: #a5c715;
  --brand-soft: #eff6d8;
  /* Lime is a light colour — white type on it lands near 1.9:1. Anything
     sitting ON the brand uses this ink, and brand-coloured *text* on a light
     canvas uses --brand-deep, because the lime itself is too pale to read. */
  --on-brand: #10130a;
  --brand-deep: #6e8a0e;
  --blue: #4b8ef5;
  --amber: #f5a524;
  --red: #e5484d;
  --green: #30a46c;

  /* THE SERIES COLOURS EVERY CHART DRAWS WITH.
   *
   * They were a magenta-and-violet ramp living in charts.js — `#b5179e` on the
   * Takings area and the payment donut, `#7209b7` on the ranked bars — which
   * belonged to no part of the brand and is what the venue meant by "change
   * the tiles to our Green from the light lilac/purple".
   *
   * Green leads, because the first series on a single-series chart is the
   * takings and the takings are the brand. The rest are hue-spaced far enough
   * apart to survive being read as a donut, and they are NOT tints of the
   * green: eight shades of one hue is a chart nobody can read a legend off.
   *
   * Deeper than the brand lime on purpose. `--brand` is #a5c715, which is a
   * 2.1:1 line against white — fine as a large fill, unreadable as a 2px
   * stroke. The dark theme puts the lime itself back, where it is the one that
   * reads. */
  --chart-1: #7fa50c;
  --chart-2: #3b7ddd;
  --chart-3: #d9860a;
  --chart-4: #0d8f8f;
  --chart-5: #7c5cd6;
  --chart-6: #d33b41;
  --chart-7: #5b7186;
  --chart-8: #96701a;

  /* Surfaces the cards sit on and inside. `--panel` and `--surface-2` were
     already being referenced with literal fallbacks in a dozen places before
     they existed; naming them here is what makes the dark theme below a list
     of colours rather than a rewrite. */
  --panel: #ffffff;
  --surface-2: #f7f8fa;
  --input-bg: #ffffff;
  --hover: #fbfafc;
  --ink: var(--text);
  --flash: #fff4d6;
  --track: #eeecf1;

  /* Status washes. Held as tokens because a badge that reads as a pale tint on
     white has to become a dark tint on near-black, and the ink on it moves the
     other way. */
  --ok-wash: #e3f6ec;
  --warn-wash: #fff3e0;
  --warn-ink: #b06d00;
  --bad-wash: #fdeceb;

  --radius: 12px;
  --shadow: 0 1px 2px rgba(20, 18, 26, .04), 0 4px 16px rgba(20, 18, 26, .04);

  /* One interior padding for every card, whatever is inside it — a form, a
     stat, a table. Before this only `.panel` and `.inspector` carried one and
     `.rd-card` carried none, so seven whole sections put their headings hard
     against the border while the rest breathed. A token rather than a number
     in six places, because the next card type will forget too. */
  --card-pad: 20px;
  /* The gap between the blocks stacked down a view. */
  --stack: 18px;
}

* { box-sizing: border-box; }

/* An explicit `display` on an element beats the `hidden` attribute, so the
   login screen stayed on top of the app forever. This puts hidden back in
   charge. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", sans-serif;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.01em; }
h2 { font-size: 24px; }
h3 { font-size: 16px; }
.muted { color: var(--muted); }
.small { font-size: 12.5px; }
.right { text-align: right; }
.error { color: var(--red); font-size: 13px; min-height: 18px; margin: 10px 0 0; }

/* ---------- Login ----------
   Split screen: form left, brand stage right.

   The old version used `minmax(420px, 44%) 1fr` with a full-bleed cover photo.
   On a 27" display that gave a ~1100px white column holding a 380px form — a
   lake of dead space — while `background-size: cover` cropped the photo to a
   meaningless slice. Both halves are now capped: the form column tops out via
   clamp(), and the stage centres its own bounded composition. Neither one
   keeps growing once the screen is bigger than the content needs. */

.login-wrap {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  /* Never narrower than the form plus its gutters, never so wide that the
     form drifts away from the fold. */
  grid-template-columns: clamp(400px, 42vw, 620px) 1fr;
  background: var(--card);
}

.login-side {
  display: grid;
  place-items: center;
  padding: clamp(28px, 5vw, 56px);
}

.login-card { width: 100%; max-width: 384px; }

/* The mark, sized the responsive way round: a width it may not exceed and a
   height that follows from it, rather than a fixed height and whatever width
   the aspect ratio makes of it. The lockup is 900x130 — nearly seven to one —
   so a "34px tall" logo is 235px wide, and on a narrow screen that is a picture
   which cannot shrink pushing everything beside it out of the way. Each of
   these is a flex item, and an image with an intrinsic width does not shrink
   below it unless it is told it may. */
.login-logo {
  display: block;
  width: 100%;
  /* 148, not 220.
   *
   * The reference measurement (reference_design/VesopaOauthReference.md §3,
   * `…716`) is that Google's own mark on its sign-in page is about 48px — small,
   * top-left, not a wordmark across the page. The complaint that started this
   * was a lockup rendered ~830px wide on a 1080px screen, which is a logo
   * taking a third of the height of a sign-in form. This is a lockup rather
   * than a square mark, so it cannot go all the way down to 48, but it has no
   * business being the largest thing on the page either. */
  max-width: 148px;
  height: auto;
  margin: 0 0 26px;
}

/* AND IT HAS TO WIN, which the rule above did not.
 *
 * `.lockup-light` and `.lockup-dark` are declared later in this file with
 * `max-width: 100%`, and they have exactly the same specificity — so the later
 * rule won and the lockup rendered 358px wide on a 414px screen. Measured, not
 * guessed: the served stylesheet said 148px while the element said 358px,
 * which is the shape of every specificity bug.
 *
 * The two-class selector (0,2,0) settles it rather than relying on source
 * order, which the next person to move a block would silently undo. */
.login-logo.lockup-light,
.login-logo.lockup-dark { max-width: 148px; }

/* ---------------------------------------------------------------------------
   Continue with Vesopa
   ---------------------------------------------------------------------------
   Built to match the Google and Apple buttons the owner supplied: a pill, a
   1px outline, the mark on the left at 20px, and the label centred with the
   mark rather than centred in the button. The same shape everywhere it appears
   is the point — a person should recognise it on the till, in the menu and in
   the back office without reading it.
   --------------------------------------------------------------------------- */
.sso-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-decoration: none;
}
.sso-mark {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  display: block;
  flex: 0 0 auto;
}

/* With "Vesopa only" on, the local form is not merely hidden — it is removed
   from the layout, so the card does not keep a gap where a password box used
   to be. */
.vesopa-only [data-local-auth] { display: none !important; }
/* And the other way round: the line that only makes sense once Vesopa is the
   only door. `hidden` in the markup keeps it away from every venue that has
   not moved over, without a second flag to set. */
.vesopa-only [data-vesopa-only] { display: block !important; }

.login-card h1 {
  font-size: clamp(24px, 2.2vw, 28px);
  letter-spacing: -.02em;
  margin-bottom: 6px;
}
.login-sub { color: var(--muted); margin: 0 0 28px; font-size: 14.5px; }

/* The app's forms space their inputs with a global `input { margin-top: 6px }`.
   Login opts out of it: the show/hide toggle is centred against the input's
   own box, and a margin on the input (which collapses through .field-pw)
   would drag that centre off by a few pixels. The gap moves to the label. */
.field { margin-top: 18px; }
.field:first-of-type { margin-top: 0; }
.field > label { display: block; margin: 0 0 6px; }
.field input { margin-top: 0; }

/* The toggle sits inside the input's box; the padding-right on the input is
   what keeps a long password from running underneath it. */
.field-pw { position: relative; }
.field-pw input { padding-right: 66px; }

.pw-toggle {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  border: 0;
  background: transparent;
  padding: 6px 8px;
  border-radius: 7px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
}
.pw-toggle:hover { color: var(--text); background: var(--bg); }

.login-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
  font-size: 13.5px;
}

/* A button, not an <a href="#">, because there is no reset page to link to —
   it discloses the hint below instead. Styled to read as a link anyway. */
.link-quiet {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 13.5px;
  color: var(--brand-deep);
  font-weight: 550;
  cursor: pointer;
}
.link-quiet:hover { text-decoration: underline; }

.login-hint {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: 9px;
  background: var(--brand-soft);
  color: #3f4d16;
  font-size: 12.5px;
  line-height: 1.5;
}
.login-hint strong { font-weight: 650; }

.check {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--muted);
  cursor: pointer;
}
.check input { width: auto; margin: 0; accent-color: var(--brand-deep); }

.login-foot {
  margin-top: 26px;
  text-align: center;
  font-size: 13.5px;
  color: var(--muted);
}
.login-foot a { color: var(--brand-deep); font-weight: 600; text-decoration: none; }
.login-foot a:hover { text-decoration: underline; }
.logo { font-size: 26px; font-weight: 750; letter-spacing: -.02em; }
.logo span { color: var(--brand); }

/* ---------- Login stage ----------
   Near-black ground with the brand's lime arc sweeping through it, echoing
   the curve in the Vesopa key visual. The photo is a crop of that same
   visual with the lockup removed, so the wordmark is never doubled. */

.login-stage {
  position: relative;
  overflow: hidden;
  display: grid;
  place-items: center;
  padding: clamp(40px, 4vw, 72px);
  /* The glow sits behind where the photo lands, so the product looks lit
     rather than pasted onto a flat panel. An earlier version put it in the
     top-right corner, which only made the empty area above look muddy. */
  background:
    radial-gradient(70% 45% at 50% 34%, rgba(165, 199, 21, .17) 0%,
                    rgba(165, 199, 21, 0) 70%),
    linear-gradient(165deg, #16190f 0%, #0a0c07 100%);
}

/* The arc, echoing the curve that sweeps under the logo in the Vesopa key
   visual. A plain ring clipped by the panel: no banding on wide gamuts, and
   it stays smooth at any size because nothing about it is rasterised. */
.login-stage::before,
.login-stage::after {
  content: '';
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
}
.login-stage::before {
  width: 165%;
  aspect-ratio: 1;
  left: -34%;
  top: 46%;
  border: 3px solid rgba(165, 199, 21, .38);
  /* Keeps the sweep where it enters bottom-left and lets it dissolve before
     it can close into an obvious circle. */
  -webkit-mask-image: linear-gradient(105deg, #000 8%, transparent 58%);
          mask-image: linear-gradient(105deg, #000 8%, transparent 58%);
}
/* A second, fainter arc just inside it — reads as motion rather than a
   drawn shape. */
.login-stage::after {
  width: 148%;
  aspect-ratio: 1;
  left: -25%;
  top: 58%;
  border: 1px solid rgba(165, 199, 21, .22);
  -webkit-mask-image: linear-gradient(105deg, #000 8%, transparent 50%);
          mask-image: linear-gradient(105deg, #000 8%, transparent 50%);
}

.stage-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  /* The cap that fixes the wide-screen problem: past this the stage grows,
     the composition inside it does not. */
  max-width: 520px;
}

.stage-shot {
  margin: 0 0 40px;
  border-radius: 18px;
  overflow: hidden;
  /* The source crop is 620×522. Held at its own aspect ratio it never
     stretches, and it is never scaled past roughly 1:1 on a 1x display. */
  aspect-ratio: 620 / 522;
  box-shadow:
    0 2px 6px rgba(0, 0, 0, .4),
    0 30px 70px -20px rgba(0, 0, 0, .8);
  outline: 1px solid rgba(255, 255, 255, .08);
  outline-offset: -1px;
}
.stage-shot img { display: block; width: 100%; height: 100%; object-fit: cover; }

.stage-title {
  color: #fff;
  font-size: clamp(26px, 2.5vw, 34px);
  line-height: 1.2;
  letter-spacing: -.025em;
  font-weight: 650;
  margin: 0 0 22px;
}

.stage-points {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 11px;
  color: rgba(255, 255, 255, .68);
  font-size: 14.5px;
}
.stage-points li {
  position: relative;
  padding-left: 26px;
  line-height: 1.45;
}
/* A lime tick, drawn rather than shipped as an icon font. */
.stage-points li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 5px;
  width: 5px;
  height: 10px;
  border: solid var(--brand);
  border-width: 0 2px 2px 0;
  transform: rotate(43deg);
}

/* Short viewports run out of vertical room long before horizontal. Shrinking
   the whole composition keeps the photo — a 1280×800 laptop is a normal way to
   open this, and dropping the product shot there loses the point of the panel.
   Only when there is genuinely no room does the photo go. */
@media (max-height: 860px) {
  .stage-inner { max-width: 430px; }
  .stage-shot { margin-bottom: 32px; }
}
@media (max-height: 740px) {
  .stage-inner { max-width: 360px; }
  .stage-shot { margin-bottom: 26px; }
  .stage-title { font-size: clamp(22px, 2vw, 27px); margin-bottom: 18px; }
  .stage-points { font-size: 13.5px; gap: 9px; }
}
@media (max-height: 600px) {
  .stage-shot { display: none; }
  .stage-inner { max-width: 420px; }
}

/* Below a tablet the stage has nowhere to go; the form takes the screen. */
@media (max-width: 980px) {
  .login-wrap { grid-template-columns: 1fr; }
  .login-stage { display: none; }
}

/* On a small phone "Keep me signed in" and "Forgot password?" together are
   wider than the column, and flex items do not shrink past their text — so
   let the row break instead of pushing the page sideways. */
@media (max-width: 400px) {
  .login-side { padding: 24px 20px; }
  .login-row { flex-wrap: wrap; gap: 10px 12px; }
}

label {
  display: block;
  margin-top: 18px;
  font-size: 13px;
  font-weight: 550;
  color: var(--muted);
}
input, select, textarea {
  width: 100%;
  margin-top: 6px;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: 9px;
  /* Stated, not left to the browser and not inherited.
     A form control does not inherit type: left alone, a text box is drawn in
     the browser's own default — Arial on Windows — while a date field, which
     has `font: inherit` further down so that iOS stops styling it, took the
     weight of the label it sits inside. Measured on the live site: text inputs
     at 400 in Arial, date inputs at 550 and 600 in the page's own stack. Two
     kinds of control in one form, in two typefaces, one of them bold.

     The value somebody typed is body text, wherever it is typed. */
  font-family: inherit;
  font-weight: 400;
  font-size: 15px;
  background: var(--input-bg);
  color: var(--text);
}
input:focus, select:focus {
  outline: none;
  border-color: var(--brand);
  /* Left over from an earlier teal palette — the ring now matches the lime it
     is drawn around. */
  box-shadow: 0 0 0 3px rgba(165, 199, 21, .28);
}

.btn {
  border: 0;
  border-radius: 9px;
  padding: 10px 18px;
  font-size: 14.5px;
  font-weight: 600;
  cursor: pointer;
  background: var(--line);
  color: var(--text);
}
.btn.primary { background: var(--brand); color: var(--on-brand); }
.btn.danger { background: var(--red); color: #fff; }
.btn.ghost { background: transparent; border: 1px solid var(--line); }
.btn.block { width: 100%; margin-top: 24px; padding: 12px; }
.btn:hover { filter: brightness(1.06); }
.btn.small { padding: 6px 12px; font-size: 13px; }

/* ---------- Shell ---------- */

#app { display: flex; min-height: 100vh; }

.rail {
  width: 236px;
  flex: none;
  background: var(--rail);
  color: var(--rail-text);
  display: flex;
  flex-direction: column;
  padding: 22px 0;
}
.rail .logo { padding: 0 22px 22px; color: var(--rail-active); font-size: 20px; }

.rail nav { display: flex; flex-direction: column; gap: 2px; padding: 0 12px; }
.nav-group {
  padding: 18px 10px 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .08em;
  /* Was #5f596b, which is a dark-theme grey. On a white rail it was invisible,
     and the sections read as gaps rather than as headings. */
  color: var(--rail-group);
  font-weight: 700;
}
.nav {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--rail-text);
  font-size: 14.5px;
  cursor: pointer;
}
.nav:hover { background: var(--rail-hover); color: var(--rail-active); }
.nav.active { background: var(--brand); color: var(--on-brand); font-weight: 600; }

.rail-foot {
  margin-top: auto;
  padding: 18px 22px 0;
  border-top: 1px solid var(--rail-line);
  display: flex;
  align-items: center;
  gap: 8px;
}
.link {
  border: 0;
  background: none;
  color: var(--rail-text);
  cursor: pointer;
  font-size: 13px;
  padding: 0;
  text-decoration: underline;
  margin-left: auto;
}

/* The operator must be able to tell at a glance whether the figures in front
   of them are live or stale. */
.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.dot.online { background: #35d07f; box-shadow: 0 0 0 3px rgba(53, 208, 127, .18); }
.dot.offline { background: #6b6478; }

/* `min-width: 0` is the whole reason the back office used to slide under its
   own sidebar on an iPad.

   A flex item defaults to `min-width: auto`, which means it refuses to become
   narrower than its content. One wide table therefore made <main> wider than
   its share, which made #app wider than the viewport, which gave the *page* a
   horizontal scrollbar — and the rail is `position: sticky`, which only sticks
   vertically. Scroll right to reach a Delete button and the rail stays put
   while the content slides underneath it, cutting the heading in half. That is
   exactly the screenshot: "epartments", "ag rows to set the order".
   
   Nothing may make this column wider than the space it has. Anything too wide
   scrolls inside its own box — see the table rule below. */
main { flex: 1; min-width: 0; padding: 30px 36px 60px; overflow: auto; }

.page-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}
/* The heading and its paragraph take what is left; the buttons keep their own
   width and drop to a line of their own when there is not enough for both.
   Without this the buttons were the half that gave way, and a page head with
   five of them (screen programming) came out as a ragged vertical stack down
   the right-hand edge with the description text running underneath it. */
.page-head > div:first-child { flex: 1 1 min(100%, 380px); min-width: 0; }
.page-head > .denom-actions,
.page-head > div:last-child:not(:first-child) {
  flex: 0 0 auto;
  margin-top: 0;
  justify-content: flex-end;
}

/* Vertical rhythm down a view.

   Cards carry no margin of their own, so two of them in a row touched unless
   somebody remembered an inline `margin-top` — which some sections had and
   others did not. Sibling margins collapse, so the page head's larger gap and
   the stat strip's still win; this only fills the gaps nobody set. */
.view > * + * { margin-top: var(--stack); }

/* ---------- Cards & stats ---------- */

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 16px;
  margin-bottom: 28px;
}
.stat { padding: 18px 20px; display: flex; flex-direction: column; gap: 8px; }
.stat .label { font-size: 13px; color: var(--muted); font-weight: 550; }
.stat .value { font-size: 27px; font-weight: 700; letter-spacing: -.02em; }
.stat .accent { height: 3px; width: 34px; border-radius: 3px; background: var(--brand); }
.stat.blue .accent { background: var(--blue); }
.stat.amber .accent { background: var(--amber); }
.stat.red .accent { background: var(--red); }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 1100px) { .grid-2 { grid-template-columns: 1fr; } }

.panel { padding: var(--card-pad); }
.panel h3 { margin-bottom: 14px; }

/* ---------- Tables ---------- */

table { width: 100%; border-collapse: collapse; }
th, td {
  padding: 12px var(--card-pad);
  text-align: left;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}
th {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  font-weight: 650;
  background: var(--hover);
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--hover); }
td.right, th.right { text-align: right; }
/* A table wider than its card scrolls inside the card. At EVERY width.

   This rule existed already and was inside `@media (max-width: 960px)`, which
   is the one thing wrong with it: an iPad in landscape is 1024–1366 CSS pixels,
   so the device this was reported from was the device the fix excluded. A wide
   table can overflow a 1440px desktop too — the width it needs depends on the
   catalogue, not on the screen.

   `overflow: hidden` used to sit on the table itself, to keep its corners
   inside the rounded card. What it actually did was clip: a product called
   Carlsberg came out as "Carli", a price as "3.0(", and the Edit / Duplicate /
   Delete buttons were painted outside the card where nothing could reach them.
   The card clips now, and it scrolls. */
.card:has(> table) { overflow-x: auto; }
.card > table {
  border-radius: var(--radius);
  /* At least fill the card; wider if the content needs it. The standard
     responsive-table pairing with the scroll container above — and it is what
     stops every cell being squeezed until "Store Manager", a gift-card code and
     "Catering, 20 covers" each wrap onto three lines. */
  min-width: 100%;
  width: max-content;
}
/* …with a ceiling, so one long note cannot make a table two thousand pixels
   wide. Anything past this wraps; everything else stays on one line. */
.card > table td { max-width: 42ch; }

/* A row's buttons are one line, never a stack. With the table at its natural
   width they already fit; this is the belt to that braces, because a stack of
   buttons overlapping a card's rounded corner is the thing that looks broken. */
td .btn { white-space: nowrap; }

/* ---- …and they do not scroll away either ---------------------------------

   The rule above lets a wide table scroll inside its card, which is right. What
   it does not say is which column goes first, and the answer is always the last
   one — which on every list in this application is the row's own buttons. So on
   an iPad, Edit and Delete on Departments, Top up and History on Gift cards,
   Edit on Deposits, Delete on Automation rules and all six icons on Scheduled
   reports were painted past the right-hand edge of the card, with nothing on
   screen to say they were there. Six separate bug reports, one cause.

   The catalogue is the worst of them: 1303px of table in a 776px card on an
   iPad in portrait. Scrolling 527px to reach Delete also takes the product's
   name off the left-hand side, so by the time the button is under your thumb
   the row no longer says what it is — which is how you delete the wrong one.

   The actions column stays put. Nothing has to be scrolled to act on a row, so
   the column that says which row it is stops being scrolled away to get there.

   Asked of the markup — `.row-actions-cell` is put on the last cell by
   cardsOnPhone() for the phone layout — so a list added later gets this without
   anyone remembering to, and a table whose last column is data (Stock, the
   report tables) is left alone. Not scoped to `.card > table`, because
   Scheduled reports puts its table inside a `.rr-scroll` of its own and that
   is the very table with six buttons in the cell. */
table:has(tbody > tr > .row-actions-cell) tr > :last-child {
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--card);
  /* `border-collapse: collapse` and `position: sticky` disagree: a collapsed
     border belongs to the table, not the cell, so it does not travel with a
     cell that has been lifted out of the flow and the column loses its row
     lines. Drawn inside the cell instead. The second shadow is the edge the
     column floats above — soft enough to read as a seam when the table fits
     and as a lifted edge when it does not. */
  box-shadow:
    inset 0 -1px 0 var(--line),
    -10px 0 12px -10px rgba(20, 18, 26, .28);
}
table:has(tbody > tr > .row-actions-cell) thead tr > :last-child {
  /* Above its own column, and above the rest of the heading row. */
  z-index: 2;
  background: var(--hover);
}
/* The hover wash is painted on the row, and the row is underneath this cell. */
table:has(tbody > tr > .row-actions-cell) tbody tr:hover > :last-child {
  background: var(--hover);
}
/* The last row owns the card's bottom corners; a line drawn under it sits on
   the border and reads as a stray rule. */
table:has(tbody > tr > .row-actions-cell) tbody tr:last-child > :last-child {
  box-shadow: -10px 0 12px -10px rgba(20, 18, 26, .28);
}
td .btn + .btn { margin-left: 6px; }

/* A row that just arrived over the socket. */
@keyframes flash { from { background: var(--flash); } to { background: transparent; } }
tr.new { animation: flash 1.6s ease-out; }

/* ---------- Bars (a chart without a chart library) ---------- */

.bars { display: flex; flex-direction: column; gap: 12px; }
.bar-row { display: grid; grid-template-columns: 120px 1fr 76px; align-items: center; gap: 12px; }
.bar-track { height: 9px; background: var(--track); border-radius: 5px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 5px; background: var(--brand); }
.bar-row .amount { text-align: right; font-variant-numeric: tabular-nums; font-size: 13.5px; }

/* ---------- Badges ---------- */

.badge {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
}
.badge.active { background: var(--ok-wash); color: var(--green); }
.badge.paused { background: var(--bad-wash); color: var(--red); }
.badge.archived { background: var(--track); color: var(--muted); }
.badge.due { background: var(--warn-wash); color: var(--warn-ink); }
.badge.paid { background: var(--ok-wash); color: var(--green); }
.badge.overdue { background: var(--bad-wash); color: var(--red); }

/* ---------- Modal ---------- */

.modal-back {
  position: fixed;
  inset: 0;
  background: rgba(20, 18, 26, .45);
  display: grid;
  place-items: center;
  z-index: 50;
}
.modal {
  width: 440px;
  max-height: 86vh;
  overflow: auto;
  background: var(--card);
  border-radius: 14px;
  padding: 26px;
  box-shadow: 0 30px 70px rgba(0, 0, 0, .3);
}
.modal h3 { margin-bottom: 6px; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; }
.row { display: flex; gap: 12px; }
.row > * { flex: 1; }
/* Stretch is the flex default, and in a row mixing a label-above-a-field with
   anything of its own height — a button, a checkbox — it aligns the tops of
   boxes that are not the same height and leaves the shorter control floating
   above the fields it belongs to. Controls line up on the bottom edge, which
   is where the fields end. Same rule as `.tbl-bar` and `.sp-bar`, for the same
   reason. */
.row { align-items: flex-end; }

.empty { padding: 40px; text-align: center; color: var(--muted); }

.thumb {
  width: 26px;
  height: 26px;
  border-radius: 5px;
  object-fit: cover;
  vertical-align: -8px;
  margin-right: 6px;
}
.emoji { font-size: 20px; vertical-align: -3px; margin-right: 4px; }

.img-field { display: flex; align-items: center; gap: 12px; margin-top: 6px; }
.img-preview {
  width: 54px;
  height: 54px;
  border-radius: 8px;
  object-fit: cover;
  border: 1px solid var(--line);
}

.swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 3px;
  margin-right: 6px;
  vertical-align: -1px;
  border: 1px solid rgba(0, 0, 0, .12);
}

/* ---------- Brand ---------- */

/* 192px is the rail's content box: 236 wide, less 22 of padding either side.
   At `height: 30px` this mark came out 208px and had been hanging 16px out of
   its own column. */
.brand-logo { display: block; width: 100%; max-width: 192px; height: auto; }
.login-card .brand-logo { max-width: 240px; margin: 0 auto 6px; }

/* ---------- Floor designer ---------- */

.designer {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 250px;
  gap: 20px;
  align-items: start;
}
/* An iPad in landscape has about 700px left after the rail and the inspector,
   which is not a floor plan — it is a third of one. The inspector goes under
   the canvas well before the layout gets that tight.

   `minmax(0, 1fr)` above rather than `1fr` for the same reason <main> needs
   `min-width: 0`: a grid track sized `1fr` still refuses to go below its
   content's minimum, so the canvas pushed the whole page sideways. */
@media (max-width: 1200px) {
  .designer { grid-template-columns: minmax(0, 1fr); }
}

/* The canvas is a grid so tables land on whole units — a free-pixel drag looks
   tidy to whoever drew it and ragged to everyone else.

   IT SCROLLS, AND THE ROOM HAS A FIXED SIZE. Both halves of that were wrong,
   and together they are the fault reported from an iPad: "the Table Designer is
   cut off and I cannot go to the right side, so I cannot set anything there".

   `overflow: hidden` meant a table beyond the visible width was painted and
   then clipped — there, on the till, and unreachable here. And the room had no
   size of its own: it was as wide as the browser happened to be, so a plan
   arranged on a desktop had tables outside the room on a tablet. A floor plan
   has to mean the same thing on every screen, because it describes a floor.

   So the room is a fixed number of grid units (see ROOM_COLS in app.js, which
   also stretches it to hold whatever a venue has already saved), and the box
   around it scrolls. `background-attachment: local` keeps the grid lines under
   the tables as it scrolls rather than painting them on the window. */
#canvas {
  position: relative;
  height: 620px;
  max-height: 70vh;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  background-color: var(--hover);
  background-image:
    linear-gradient(to right, var(--line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--line) 1px, transparent 1px);
  background-size: 40px 40px;
  background-attachment: local;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* The room itself. Sized by app.js so it is never smaller than the plan it has
   to hold — a spacer rather than a wrapper element, so the tables keep their
   existing absolute coordinates and nothing about the saved data changes. */
#canvas::after {
  content: '';
  display: block;
  width: var(--room-w, 960px);
  height: var(--room-h, 600px);
}

/* `touch-action: none` so a finger on a table drags the table. Without it the
   browser claims the gesture and scrolls the canvas instead — which is the
   right thing to do on the empty grid between tables, and exactly the wrong
   thing on a table. Both work now: drag a table to move it, drag the floor to
   look around it. */
.tbl {
  position: absolute;
  display: grid;
  place-items: center;
  touch-action: none;
  background: var(--card);
  border: 2px solid var(--brand);
  color: var(--text);
  border-radius: 8px;
  cursor: grab;
  user-select: none;
  box-shadow: var(--shadow);
  transition: box-shadow .12s;
}
.tbl:hover { box-shadow: 0 4px 14px rgba(165, 199, 21, .28); }
.tbl.dragging { cursor: grabbing; opacity: .85; z-index: 10; }
.tbl.circle { border-radius: 50%; }
.tbl.selected { border-color: var(--amber); box-shadow: 0 0 0 3px rgba(245, 165, 36, .25); }
.tbl .num { font-weight: 700; font-size: 17px; }
.tbl .seats { font-size: 11px; color: var(--muted); }

.room-tabs { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
.room-tab {
  padding: 7px 14px;
  border: 1px solid var(--line);
  background: var(--card);
  border-radius: 20px;
  cursor: pointer;
  font-size: 13.5px;
}
.room-tab.active { background: var(--brand); color: var(--on-brand); border-color: var(--brand); }

.inspector { padding: var(--card-pad); }
.inspector h3 { margin-bottom: 12px; }
.inspector label { margin-top: 12px; }
.hint {
  font-size: 12.5px;
  color: var(--muted);
  background: var(--brand-soft);
  padding: 10px 12px;
  border-radius: 8px;
  margin-top: 14px;
}
.unsaved { color: var(--amber); font-weight: 600; }

/* ---------- Fixed, collapsible sidebar ---------- */

/* The rail stays put while the page scrolls, and scrolls internally when the
   nav is taller than the viewport, so long menus never push the sign-out
   button off-screen. */
.rail {
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100vh;
  overflow: hidden;
}
.rail nav {
  flex: 1;
  overflow-y: auto;
  /* Room under the last item. Without it the scroll ends flush against the
     fold button and the bottom heading is cut through the middle of its own
     letters, which reads as a rendering fault rather than as more to scroll. */
  padding-bottom: 14px;
  /* A thin scrollbar that does not draw attention on the dark rail. */
  scrollbar-width: thin;
  scrollbar-color: var(--rail-scroll) transparent;
}

/* Finding a page now that every section starts closed.
   ---------------------------------------------------------------------------
   The venue asked for the groups to be folded, and they are right — forty-nine
   items is not a menu. But folding them costs something real: somebody who
   knows a page is called "Timesheets" now has to remember it lives under
   Reports before they can get to it.

   Typing here shows every matching item wherever it lives, folded or not, and
   clearing it puts the rail back exactly as it was. */
/* Which site this session is in -- a login with more than one venue. Styled
   like the find box beneath it, so the rail reads as one column of controls. */
.site-switch {
  margin: 0 12px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.site-switch label {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--rail-group);
}
.site-switch select {
  width: 100%;
  min-height: 38px;
  font-size: 16px; /* 16 or Safari zooms in and stays there -- see .rail-find */
  border-radius: 8px;
  border: 1px solid var(--rail-line);
  background: var(--rail-hover);
  color: var(--rail-active);
  padding: 4px 8px;
}

.rail-find {
  margin: 0 12px 8px;
  position: relative;
}
.rail-find input {
  width: 100%;
  min-height: 38px;
  height: 38px;
  padding: 4px 10px 4px 30px;
  /* Sixteen, not the 13 the rail's labels use. Safari zooms the whole page in
     when a field under 16px takes focus and does not zoom back out, so every
     input in this product is at least that — see the guard in
     test/backoffice-tablet.test.js, which caught this one. */
  font-size: 16px;
  border-radius: 8px;
  border: 1px solid var(--rail-line);
  background: var(--rail-hover);
  color: var(--rail-text);
}
.rail-find input::placeholder { color: var(--rail-group); }
.rail-find input:focus {
  outline: none;
  border-color: var(--brand);
  color: var(--rail-active);
}
.rail-find .rail-find-icon {
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  opacity: .55;
  pointer-events: none;
}
/* While a search is running the headings step back: what is on screen is a
   list of matches, not a set of sections. */
.rail.finding .nav-group { opacity: .45; }
.nav.hidden-by-find,
.nav-group.hidden-by-find { display: none; }

/* A match shows even though its section is folded.
   ---------------------------------------------------------------------------
   This is the whole feature, and without it the search found nothing: every
   group starts collapsed, so every matching item carried `hidden-by-group` as
   well, and typing "time" produced the Reports heading with nothing under it.
   Three classes, so it outranks `.nav.hidden-by-group`. */
.rail.finding .nav.hidden-by-group:not(.hidden-by-find) { display: flex; }
.rail nav::-webkit-scrollbar { width: 6px; }
.rail nav::-webkit-scrollbar-thumb {
  background: var(--rail-scroll);
  border-radius: 3px;
}

/* A nav group heading is now a button that folds its section away. The caret
   rotates to show state; a collapsed group hides every .nav until the next
   heading. */
.nav-group {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  border: 0;
  background: none;
  cursor: pointer;
  user-select: none;
}
.nav-group .caret {
  transition: transform .15s ease;
  font-size: 9px;
  opacity: .7;
}
.nav-group.collapsed .caret { transform: rotate(-90deg); }
.nav.hidden-by-group { display: none; }

/* ---------- Mobile: off-canvas rail ----------
   The rail is a fixed 236px column, so on a phone it left ~140px for the
   content and every table scrolled sideways under it. Below 960px it becomes a
   drawer over the page instead: hidden by default, slid in by #rail-toggle in
   the top bar, dismissed by the scrim, Escape, or picking a view. */

/* Desktop keeps the rail permanently open, so the mobile chrome is not rendered
   at all — no stray toggle, no scrim capturing clicks. */
.topbar,
.rail-scrim { display: none; }

@media (max-width: 960px) {
  .topbar {
    display: flex;
    align-items: center;
    gap: 12px;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 56px;
    padding: 0 14px;
    background: var(--rail);
    z-index: 45;
  }
  /* THE LOCKUP IS CENTRED, AND IT IS 147px.
   *
   * `max-width: 220px` here was being beaten by `max-width: 100%` on the
   * .lockup-light / .lockup-dark classes further down the stylesheet — same
   * specificity, later in the file, so the later one won. The wordmark came out
   * 294px wide in a 390px bar, ran under the theme button and had its last
   * letter cut off.
   *
   * Centred between the menu button and whatever is on the right, rather than
   * pushed left, so the bar reads as a bar and not as a logo with things
   * hanging off it. The two auto margins are what centre it; the min-width:0
   * is what lets it shrink rather than push the buttons off the end.
   */
  /* Sizing only. `display` belongs to .lockup-light / .lockup-dark, which pick
     the legible wordmark for the theme — setting it here outranked them and put
     both lockups on the bar at once. */
  .topbar-logo,
  .topbar-logo.lockup-light,
  .topbar-logo.lockup-dark {
    width: 100%;
    max-width: 147px;
    height: auto;
    min-width: 0;
    margin-left: auto;
    margin-right: auto;
  }

  /* The theme control sits in this bar rather than over it, and the bar keeps
     a lane for it so the lockup stays centred against the space that is left. */
  .topbar { padding-right: 58px; }

  .rail-toggle {
    width: 40px;
    height: 40px;
    flex: none;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 9px;
    /* Was rgba(255,255,255,.08) — invisible now that Day mode paints this bar
       white. The bar is --rail, so its furniture has to come from the same
       place. */
    background: var(--rail-hover);
    color: var(--rail-active);
    cursor: pointer;
    /* Comfortably past the 44px touch target once the padding is counted. */
    margin: 0 -2px;
  }
  /* Three bars drawn from one element: the middle is the box, the outer two are
     its pseudo-elements, so the button needs no inner markup to animate. */
  .rail-toggle-bars,
  .rail-toggle-bars::before,
  .rail-toggle-bars::after {
    content: "";
    display: block;
    width: 18px;
    height: 2px;
    border-radius: 2px;
    /* Three white bars on a bar that Day mode paints white is no hamburger at
       all — which is why the menu could not be opened. */
    background: currentColor;
    transition: transform .2s ease, opacity .2s ease;
  }
  .rail-toggle-bars { position: relative; }
  .rail-toggle-bars::before { position: absolute; top: -6px; }
  .rail-toggle-bars::after { position: absolute; top: 6px; }

  /* Open state folds the bars into a cross, so the same control closes it. */
  #app.rail-open .rail-toggle-bars { background: transparent; }
  #app.rail-open .rail-toggle-bars::before { top: 0; transform: rotate(45deg); }
  #app.rail-open .rail-toggle-bars::after { top: 0; transform: rotate(-45deg); }

  #app { display: block; }

  .rail {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: 272px;
    max-width: 84vw;
    height: 100dvh;
    z-index: 60;
    transform: translateX(-100%);
    transition: transform .22s ease;
    box-shadow: 0 0 40px rgba(0, 0, 0, .4);
    /* A drawer that cannot reach its own sign-out button is worse than no
       drawer; the nav scrolls inside the fixed frame. */
    overflow: hidden;
  }
  #app.rail-open .rail { transform: none; }

  .rail-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 55;
    background: rgba(0, 0, 0, .5);
  }

  /* Clear the fixed top bar. */
  main { padding: 76px 16px 48px; }

  /* Same rhythm, tightened: 20px of inset either side of a 360px screen spends
     a ninth of the width on nothing. */
  :root { --card-pad: 16px; --stack: 14px; }
  .page-head { flex-wrap: wrap; gap: 12px; }
  h2 { font-size: 21px; }

  /* Wide report tables scroll in their own box rather than the page body —
     which `.card:has(> table)` above now does, at the card, and is why the
     table itself must NOT also be `display: block` here.

     That is what made Stock, and every other short table, stop at about a
     third of the screen with the rest of the card empty beside it. A <table>
     set to `display: block` keeps its rows and cells as table parts, so the
     browser wraps them in an anonymous table box inside the block — and that
     inner box shrink-wraps its content. `min-width: 100%` then applies to the
     outer block, which was already full width, and the columns everyone can
     actually see stay bunched against the left edge. Four short columns of
     PLUs and quantities is the worst case and Stock is exactly that.

     Left as a real table, `min-width: 100%` reaches the columns and the card
     does the scrolling when the content is genuinely wider. */
  .panel-body { display: block; overflow-x: auto; }
  th, td { padding: 10px 12px; white-space: nowrap; }

  .modal { width: 92vw; padding: 20px; }
  .row { flex-wrap: wrap; }
  .row > * { flex: 1 1 160px; }
  .designer { grid-template-columns: 1fr; }
  .bar-row { grid-template-columns: 92px 1fr 66px; }
}

/* Someone who has asked for less motion still needs the drawer — it just
   arrives rather than slides. */
@media (prefers-reduced-motion: reduce) {
  .rail, .rail-toggle-bars,
  .rail-toggle-bars::before, .rail-toggle-bars::after { transition: none; }
}

/* ---------- Drag-to-reorder rows ---------- */

.drag-col { width: 34px; }
.drag-cell { width: 34px; text-align: center; color: var(--muted); }
.drag-handle {
  cursor: grab;
  user-select: none;
  letter-spacing: -2px;
  font-size: 15px;
  line-height: 1;
  opacity: .55;
}
.drag-handle:hover { opacity: 1; }
tr.dragging { opacity: .5; background: var(--brand-soft); }
tr[draggable="true"] { cursor: grabbing; }
.nowrap { white-space: nowrap; }

/* ---------- Checkbox field in the modal ---------- */

.check-field { display: inline-flex; align-items: center; }
.check-field input[type="checkbox"] {
  width: 20px;
  height: 20px;
  accent-color: var(--brand);
  cursor: pointer;
}

/* ---------- Receipt / detail viewer ---------- */

.receipt-view {
  font-variant-numeric: tabular-nums;
  max-width: 340px;
  margin: 0 auto;
}
.receipt-view h3 { text-align: center; margin-bottom: 4px; }
.receipt-view .r-sub { text-align: center; color: var(--muted); font-size: 12.5px; margin-bottom: 14px; }
.receipt-view table { margin: 8px 0; }
.receipt-view td, .receipt-view th { padding: 5px 4px; border: 0; font-size: 13px; }
.receipt-view .r-tot td { border-top: 1px dashed var(--line); font-weight: 700; padding-top: 8px; }
.receipt-view .r-line-tot { text-align: right; }
.linkish { cursor: pointer; color: var(--brand-deep); text-decoration: underline; }

/* ---------- Image cropper ---------- */

.cropper { text-align: center; }
.crop-stage {
  position: relative;
  margin: 14px auto;
  border-radius: 10px;
  overflow: hidden;
  touch-action: none;
  /* A checkerboard, not the flat #111 this used to be. Zooming out past the
     frame is now allowed, so a picture can have clear space around it — and
     that space has to read as "nothing here" rather than as a black band the
     manager thinks they are about to upload. */
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #e9edf3 25%, transparent 25%, transparent 75%, #e9edf3 75%),
    linear-gradient(45deg, #e9edf3 25%, transparent 25%, transparent 75%, #e9edf3 75%);
  background-size: 16px 16px;
  background-position: 0 0, 8px 8px;
}
#crop-canvas { display: block; cursor: grab; }
#crop-canvas:active { cursor: grabbing; }
/* A subtle frame + darkened surround so the framed square reads as the crop. */
.crop-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .5) inset;
  border-radius: 10px;
}
.crop-zoom {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 320px;
  margin: 6px auto 4px;
  font-size: 13px;
  color: var(--muted);
}
.crop-zoom input[type="range"] { flex: 1; accent-color: var(--brand); }

tr.clickable { cursor: pointer; }
tr.clickable:hover { background: var(--brand-soft); }

/* When printing a receipt, hide everything except the receipt node so the
   browser's "Save as PDF" produces a clean slip, not the whole back office. */
@media print {
  body.printing-receipt #app,
  body.printing-receipt #login { display: none !important; }
  body.printing-receipt #modal-root .modal-back {
    position: static;
    background: none;
    display: block;
  }
  body.printing-receipt #modal-root .modal {
    box-shadow: none;
    max-width: none;
    padding: 0;
  }
  body.printing-receipt .modal-actions { display: none !important; }
}

/* A print area for the report tables (Till Report / Bill Report export). */
.report-print-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
}
@media print {
  body.printing-report .rail,
  body.printing-report .page-head .btn,
  body.printing-report #modal-root { display: none !important; }
  body.printing-report main { padding: 0; }
  body.printing-report .view[hidden] { display: none; }
}

/* ---- Receipt designer ---------------------------------------------------- */

.rd-head-actions { display: flex; gap: 8px; }

/* Editor beside a paper preview. The preview sticks so it stays visible while
   the long form is scrolled — the whole point is watching edits land. */
.rd-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 18px;
  align-items: start;
}
/* An editor with nothing to preview is one column, not one column and a hole.

   Four of the five screens built on this — Kitchen screens, Receipt designer,
   Loyalty, Tender — put a preview pane in the second track. Till & printers
   never had one: its preview is the phone-shaped panel inside the first card,
   so the 340px track sat empty and the page stopped 358px short of the right
   edge on every desktop, which is the gap that gets reported. Asked of the
   markup rather than fixed by giving the idle view a class of its own, so the
   next single-pane editor is right the day it is written. */
.rd-layout:not(:has(> * + *)) { grid-template-columns: minmax(0, 1fr); }

/* …and then it has to do something with the width it just got back.

   A settings form 1100px across is its own kind of wrong: a tick box and the
   sentence explaining it end up a monitor apart, and a printer name capped at
   forty characters gets a box wide enough for four hundred. Two columns of
   about 550px is the measure the two-pane editors already read at, so the
   single-pane one is not a different page — it is the same page using the
   space the preview pane was holding. Below 1101px the preview pane would have
   dropped under the editor anyway, and one column is right again. */
@media (min-width: 1101px) {
  .rd-layout:not(:has(> * + *)) .rd-editor {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
}

/* A set of names that belong together, laid out as a set. Seven printers down
   a page as seven full-width boxes reads as seven unrelated questions. */
.rd-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 6px 14px;
}
.rd-card .rd-fields label { margin-bottom: 4px; }

.rd-editor { display: flex; flex-direction: column; gap: var(--stack); min-width: 0; }

/* The form card. `.card` draws the box; this is what makes it habitable. */
.rd-card { padding: var(--card-pad); }
.rd-card h3 { margin: 0 0 12px; font-size: 15px; }
.rd-card label { display: block; margin-bottom: 10px; font-size: 13px; }
.rd-card label.check { display: flex; align-items: center; gap: 8px; }
.rd-card input[type="text"], .rd-card input:not([type]),
.rd-card select, .rd-card textarea {
  width: 100%; margin-top: 4px;
}
.rd-card textarea { resize: vertical; font: inherit; }

.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }

/* ---------- Note keys ----------
   One row per denomination: the picture as it will appear on the till, the
   value, the label, and the way out. The thumbnail is a real banknote crop, so
   it keeps a note's ~2:1 shape rather than being squeezed into a square. */
.denom-rows { display: flex; flex-direction: column; gap: 10px; }

.denom-row {
  display: grid;
  grid-template-columns: 104px 110px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg);
}
.denom-row label { margin: 0; }

.denom-thumb {
  width: 104px;
  aspect-ratio: 2 / 1;
  border-radius: 6px;
  border: 1px dashed var(--line);
  background: var(--card) center/cover no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 11px;
  color: var(--muted);
  text-align: center;
  overflow: hidden;
}
.denom-thumb.has-image { border-style: solid; }
.denom-thumb:hover { border-color: var(--brand); }

.denom-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
  /* Stretch is the flex default, and it is wrong the moment this row holds a
     label-above-a-field beside a plain button: on Timesheets the From and To
     columns are two lines tall, so Show and Export PDF were drawn 60px high,
     towering over the boxes they act on. Every button in here is its own
     height and sits on the same line the fields end on. */
  align-items: flex-end;
}

@media (max-width: 720px) {
  .denom-row { grid-template-columns: 84px 1fr; }
  .denom-thumb { width: 84px; }
}

.rd-logo-row { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 10px; }
.rd-logo-preview {
  width: 120px; height: 90px; flex: none;
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line, #d4d7dd); border-radius: 8px;
  background: var(--surface-2, #f7f8fa); overflow: hidden;
}
.rd-logo-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.rd-logo-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.btn.danger-ghost { color: #b3261e; }

/* A PIN read back to a member of staff who forgot it. Tabular and spaced, so
   four digits can be read aloud without a misread. */
.staff-pin { font-family: ui-monospace, Menlo, Consolas, monospace; letter-spacing: 0.14em; }
.pin-warn {
  font-family: inherit; letter-spacing: 0; font-size: 11px; font-weight: 600;
  color: #8a5300; background: #fff3d6; border-radius: 5px; padding: 1px 6px;
  white-space: nowrap;
}

/* Idle screen preview. Drawn at 16:9 on black, because that is what a till
   panel actually is — a preview on a white card would flatter an image that
   turns out to be unreadable on the terminal. */
.idle-preview {
  aspect-ratio: 16 / 9;
  width: 100%;
  max-width: 460px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line, #d4d7dd); border-radius: 10px;
  background: #000; overflow: hidden; position: relative;
}
.idle-preview img { width: 100%; height: 100%; object-fit: cover; }
.idle-preview .idle-fallback {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.idle-preview .idle-fallback img { width: 55%; height: auto; object-fit: contain; }
/* The lime rule from the splash, so the preview reads as the same screen. */
.idle-preview .idle-rule { width: 55%; height: 3px; background: #c3ff00; border-radius: 2px; }
.idle-preview .idle-msg {
  position: absolute; bottom: 10px; left: 0; right: 0; text-align: center;
  color: rgba(255, 255, 255, 0.85); font-size: 12px; letter-spacing: 0.06em;
  text-transform: uppercase;
}

.rd-preview-pane {
  position: sticky;
  top: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.rd-preview-head {
  display: flex; align-items: center; justify-content: space-between;
}
.rd-paper-wrap {
  background: var(--surface-2, #eef0f4);
  border-radius: 12px; padding: 18px;
  max-height: calc(100vh - 160px); overflow: auto;
}

/* The paper itself. Width is set in millimetres so the preview is a true
   representation of an 80mm or 58mm roll rather than an approximation. */
.rd-paper {
  width: 72mm; margin: 0 auto; padding: 5mm 4mm;
  background: #fff; color: #000;
  font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 10px; line-height: 1.35;
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);
}
.rd-paper.narrow { width: 51mm; font-size: 9px; }

.rp-logo { text-align: center; margin-bottom: 6px; }
.rp-logo img { max-height: 46px; max-width: 70%; object-fit: contain; }
.rp-name { text-align: center; font-weight: 700; font-size: 13px; letter-spacing: 0.5px; }
.rd-paper.narrow .rp-name { font-size: 11px; }
.rp-centre { text-align: center; }
.rp-note { color: #444; font-size: 0.9em; }
.rp-row { display: flex; justify-content: space-between; gap: 8px; }
.rp-row > span:first-child { flex: 1; min-width: 0; word-break: break-word; }
.rp-sub { color: #444; font-size: 0.9em; padding-left: 10px; }
.rp-italic { font-style: italic; }
.rp-rule { border-top: 1px dashed #999; margin: 5px 0; }
.rp-rule.heavy { border-top: 2px solid #000; }
.rp-total {
  display: flex; justify-content: space-between;
  font-weight: 700; font-size: 1.25em;
  background: #eee; padding: 3px 4px; margin: 4px 0;
}
.rp-footer { text-align: center; font-weight: 700; margin-top: 4px; }

/* Stand-ins: the real barcode and QR are generated by the till's PDF builder,
   and drawing them here would only duplicate that logic. */
.rp-barcode {
  height: 30px; margin: 8px auto 4px; width: 70%;
  background: repeating-linear-gradient(
    90deg, #000 0 2px, #fff 2px 4px, #000 4px 5px, #fff 5px 8px);
}
.rp-qr {
  width: 46px; height: 46px; margin: 6px auto;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid #000; font-size: 9px; letter-spacing: 1px;
}

@media (max-width: 1100px) {
  .rd-layout { grid-template-columns: 1fr; }
  .rd-preview-pane { position: static; }
  .grid2 { grid-template-columns: 1fr; }
}

/* ---- Charts -------------------------------------------------------------- */

.chart-box { width: 100%; overflow: hidden; }
.chart { width: 100%; height: auto; display: block; }
.chart-grid { stroke: var(--line, #e3e6ea); stroke-width: 1; }
.chart-axis { font-size: 11px; fill: var(--muted, #7a828c); }
.chart-empty { padding: 22px 4px; }

.donut-wrap { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
.donut { flex: none; }
.donut-total { font-size: 20px; font-weight: 700; fill: var(--ink, #16181d); }
.donut-caption { font-size: 10px; fill: var(--muted, #7a828c); letter-spacing: 1px; }

.chart-legend { list-style: none; margin: 0; padding: 0; flex: 1; min-width: 160px; }
.chart-legend li {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 0; font-size: 13px;
}
.chart-legend .swatch {
  width: 11px; height: 11px; border-radius: 3px; flex: none;
}
.chart-legend .legend-label { flex: 1; }
.chart-legend .legend-value { font-weight: 600; }

.ranked { display: flex; flex-direction: column; gap: 8px; }
.ranked-row {
  display: grid;
  grid-template-columns: 20px minmax(80px, 1.4fr) 2fr auto;
  align-items: center; gap: 10px; font-size: 13px;
}
.ranked-rank { color: var(--muted, #7a828c); font-size: 11px; text-align: right; }
.ranked-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ranked-track {
  height: 8px; border-radius: 4px;
  background: var(--surface-2, #eef0f4); overflow: hidden;
}
.ranked-fill { display: block; height: 100%; border-radius: 4px; }
.ranked-value { font-weight: 600; white-space: nowrap; }
.ranked-meta {
  grid-column: 2 / -1; font-size: 11px; color: var(--muted, #7a828c);
  margin-top: -4px;
}

/* ---- Stat cards ---------------------------------------------------------- */

.grid-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
  gap: 12px; margin-bottom: 18px;
}
.stat-card {
  display: flex; flex-direction: column; gap: 3px;
  padding: 14px 16px; border-left: 3px solid var(--line, #d9dce1);
}
/* The accent stripe down each figure.

   Five hardcoded values from a palette nothing else used — a royal blue, an
   orange, a mint and a raspberry — sat beside a brand-green first card and
   read as five unrelated cards rather than one row. They take the chart
   series colours now, so the stripe on Gross Takings is the same green as the
   Takings line under it, and the whole dashboard is one set of colours. */
.stat-card.primary { border-left-color: var(--chart-1); }
.stat-card.blue    { border-left-color: var(--chart-2); }
.stat-card.amber   { border-left-color: var(--chart-3); }
.stat-card.green   { border-left-color: var(--chart-4); }
.stat-card.red     { border-left-color: var(--chart-6); }
.stat-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--muted, #7a828c);
}
.stat-value { font-size: 22px; font-weight: 700; }
.stat-hint { font-size: 11px; color: var(--muted, #7a828c); }
.stat-delta { font-size: 12px; font-weight: 600; }
.stat-delta.up { color: #06a77d; }
.stat-delta.down { color: #d1495b; }

/* ---- Dashboard toolbar --------------------------------------------------- */

.dash-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 14px; flex-wrap: wrap;
}
.seg {
  display: inline-flex; border: 1px solid var(--line, #d9dce1);
  border-radius: 8px; overflow: hidden;
}
.seg-btn {
  border: 0; background: transparent; padding: 7px 14px;
  font: inherit; font-size: 13px; cursor: pointer;
  border-right: 1px solid var(--line, #d9dce1);
}
.seg-btn:last-child { border-right: 0; }
.seg-btn.active { background: var(--brand); color: var(--on-brand); font-weight: 600; }

/* A label in one of these is a phrase, and a phrase must never break in the
   middle of itself.
 *
 * On a touchscreen .seg-btn is locked to 40px (see the touch-target rules near
 * the end of this file). "Last 7 days" wrapping to two lines inside a 40px box
 * put "days" below the bottom of its own button and clipped it -- so a phone
 * showed "Last 7", "Last 30" and "90" with a sliver of cut-off text under each.
 * Reported from a phone; reproduced at 360, 390 and 412px. */
.seg-btn { white-space: nowrap; }

.panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 10px;
}

.mini-list { display: flex; flex-direction: column; gap: 9px; }
.mini-list > div {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 13px; padding-bottom: 8px;
  border-bottom: 1px solid var(--line, #eef0f4);
}
.mini-list > div:last-child { border-bottom: 0; padding-bottom: 0; }
.mini-list .warn b { color: #d1495b; }

/* ---- Commerce tables ----------------------------------------------------- */

.pill {
  display: inline-block; padding: 2px 9px; border-radius: 11px;
  font-size: 11px; background: var(--surface-2, #eef0f4);
  color: var(--muted, #7a828c); text-transform: capitalize;
}
.pill.on { background: #e4f7ef; color: #06784f; }
/* A trainee: amber, the colour the till's TRAINING bar is. */
.pill.training-pill { background: #fff3d6; color: #8a5a00; font-weight: 600; }

.promo-badge {
  display: inline-block; padding: 3px 9px; border-radius: 5px;
  font-size: 11px; font-weight: 700; color: #fff;
}

td code {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 12px; background: var(--surface-2, #f2f4f7);
  padding: 2px 6px; border-radius: 4px;
}
.pos { color: #06784f; }
.neg { color: #d1495b; }

/* ---- Loyalty tiers ------------------------------------------------------- */

.tier-row {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding: 9px 0; border-bottom: 1px solid var(--line, #eef0f4);
}
.tier-row .tier-name { flex: 1; min-width: 110px; }
.tier-field {
  display: flex; align-items: center; gap: 5px;
  font-size: 12px; margin: 0; color: var(--muted, #7a828c);
}
.tier-field input { width: 68px; }
.tier-row input[type="color"] {
  width: 34px; height: 30px; padding: 2px; border-radius: 6px;
}

/* ---- Tender preview ------------------------------------------------------ */

.till-preview {
  background: #12141a; color: #fff; border-radius: 10px; padding: 16px;
}
.till-preview .muted { color: #9aa3af; }
.till-row {
  display: flex; justify-content: space-between;
  font-size: 17px; padding-bottom: 10px;
  border-bottom: 1px solid #2a2f3a;
}
.till-keys { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.till-key {
  padding: 7px 11px; border-radius: 7px; font-size: 12px;
  background: #232734; border: 1px solid #313747;
}
.till-key.cash { background: #16351f; border-color: #1f5130; }

/* ---- Wide modal ---------------------------------------------------------- */

.modal.wide { max-width: 760px; width: 92vw; }
.panel-body { max-height: 62vh; overflow: auto; }
table.plain { width: 100%; border-collapse: collapse; font-size: 13px; }
table.plain th, table.plain td {
  text-align: left; padding: 7px 9px;
  border-bottom: 1px solid var(--line, #eef0f4);
}
table.plain th { color: var(--muted, #7a828c); font-weight: 600; font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.5px; }

.colour-field { display: flex; gap: 8px; align-items: center; }
.colour-field input[type="color"] {
  width: 40px; height: 34px; padding: 2px; border-radius: 6px; flex: none;
}
.colour-field input[type="text"] { flex: 1; font-family: ui-monospace, monospace; }

/* ---- Kitchen printer routing --------------------------------------------
   Six tick boxes that have to read as a set of physical printers, not as a
   list of settings. Laid out as a grid of cards so a routed station is
   legible at a glance across the form, which is how a manager checks their
   work: they scan for the lit ones rather than reading every label. */
.station-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 8px;
  margin-top: 6px;
}
.station {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  transition: background 0.12s, border-color 0.12s;
}
.station:hover { border-color: var(--brand-deep); }
/* Ticked: the lime wash plus a solid edge. Filling the whole card with the
   brand lime would make six selected stations the loudest thing on a form
   whose subject is the product, so the fill stays a wash. */
.station.on {
  background: var(--brand-soft);
  border-color: var(--brand-deep);
  font-weight: 600;
}
.station input[type="checkbox"] {
  width: auto;
  margin: 0;
  flex: none;
  accent-color: var(--brand-deep);
}
.station span { flex: 1; }

/* A routed station in the catalogue table. */
.chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--brand-soft);
  color: var(--brand-deep);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}
/* The unusual setting, so it must not read the same as a routing chip. */
.chip.warn { background: var(--bad-wash); color: var(--red); }

/* ---- Kitchen screens ------------------------------------------------------
   Vesopa EPOS Kitchen: where each station's tickets come out, the logins, the
   named boards, and a live monitor of what the kitchen is looking at. */

.kds-modes { display: flex; flex-direction: column; gap: 4px; }

.kds-mode-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
}
.kds-mode-row:last-child { border-bottom: 0; }

.kds-mode-name { font-weight: 600; }
/* The slot number under the venue's own name for it. The product editor lists
   both, and a manager cross-referencing the two screens needs the key that
   appears on each. */
.kds-mode-name span { display: block; font-weight: 400; }

/* How many products point at this station. Next to the toggle rather than in a
   tooltip, because it is the number that decides whether the toggle matters —
   a station nothing routes to can stay on Printer for ever. */
.kds-routed {
  display: block;
  font-weight: 400;
  font-size: 12px;
  color: var(--muted);
}

/* Products routed here, and still going to paper. This is the exact shape of
   the fault the page could not explain: a venue whose whole menu pointed at
   one station, that station left on Printer, and a kitchen screen that stayed
   empty all service. Amber rather than red — it is correct for a venue that
   prints, and only suspicious for one that has just set a screen up. */
.kds-stranded .kds-routed {
  color: #9a6206;
  font-weight: 600;
}
.kds-stranded .kds-routed::after {
  content: ' routed here, still going to paper';
  font-weight: 400;
}

/* The three-way control has to survive a narrow rail without wrapping into
   something unreadable, so it shrinks rather than reflows. */
.kds-mode-seg { flex: none; }
.kds-mode-seg .seg-btn { padding: 6px 12px; font-size: 12.5px; }

/* On a phone it stops shrinking and starts stacking.
   "Cold Larder" plus three buttons plus the routed-products note is more than
   360px of row, and side by side the name was down to a couple of words per
   line with the control jammed against it. The station is a heading and the
   three choices are a full-width control under it, which is also three targets
   a thumb can hit rather than three 60px ones. */
@media (max-width: 760px) {
  .kds-mode-row {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 12px 0;
  }
  .kds-mode-seg { display: flex; }
  .kds-mode-seg .seg-btn { flex: 1 1 0; padding: 9px 8px; }
}

/* ---- The live monitor -----------------------------------------------------
   Read-only, and deliberately: bumping an order is a decision made by somebody
   who can see the plate. This answers "is the kitchen actually getting these?",
   which is the first question anybody asks after moving a station to a screen. */

.kds-board {
  display: flex;
  flex-direction: column;
  gap: 9px;
  /* Bounded, because this is a panel beside a form rather than the board
     itself — a busy service would otherwise push the page metres long. */
  max-height: 520px;
  overflow-y: auto;
}

.kds-ticket {
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 9px 11px;
  background: var(--card);
}

.kds-ticket-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 5px;
}

.kds-ticket-line {
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-size: 13.5px;
  padding: 1px 0;
}

/* A fixed column, so item names line up down the ticket. A ragged left edge
   costs a fraction of a second per line to read. */
.kds-qty {
  min-width: 20px;
  text-align: right;
  font-weight: 700;
  color: var(--muted);
}

/* The modifier. Red here as on the screen itself, because it is the one thing
   on a ticket that is not the recipe — and a manager checking a complaint is
   looking for exactly this line. */
.kds-note {
  color: var(--red);
  font-style: normal;
  font-weight: 600;
}

/* ---- Kitchen: white-label branding --------------------------------------
   The colour pair and a preview of the start screen the wall will show.
   Drawn here rather than left to the app so a manager choosing a background
   and an accent can see the pair together — the two failure modes are a lime
   accent on a white ground, which vanishes, and a dark accent on a dark
   ground, which never appears at all. */

.kb-colours {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  margin-top: 12px;
  flex-wrap: wrap;
}

.kb-colours label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12.5px;
  margin: 0;
}

.kb-colours input[type='color'] {
  width: 64px;
  height: 34px;
  padding: 2px;
  border: 1px solid var(--line, #d7dbe2);
  border-radius: 6px;
  background: var(--card);
  cursor: pointer;
}

.kds-splash-preview {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: 10px;
  border: 1px solid var(--line, #d7dbe2);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-align: center;
  padding: 18px;
}

.kds-splash-preview .kds-splash-mark {
  width: 84px;
  height: 84px;
  border-radius: 19px;
  object-fit: contain;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 44px;
  line-height: 1;
}

.kds-splash-preview .kds-splash-name {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: 0.2px;
}

.kds-splash-preview .kds-splash-tagline {
  font-size: 13px;
  opacity: 0.75;
}

/* The accent's own job on the real screen: a sweep under the name. Shown here
   as a static bar, because an animation in a settings preview is a thing a
   manager waits for rather than reads. */
.kds-splash-preview .kds-splash-rule {
  width: 120px;
  height: 4px;
  border-radius: 2px;
}

.kds-splash-preview .kds-splash-powered {
  position: absolute;
  bottom: 12px;
  font-size: 11px;
  letter-spacing: 0.4px;
  opacity: 0.55;
}

/* The cropper's two shortcuts, either end of the zoom slider.
   Kept as buttons as well as slider positions because "fit the whole picture"
   is a thing a manager wants by name, not a place on a scale they are expected
   to find by dragging. */
.crop-presets {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 8px;
}

/* ---------- Screen programming ----------
   The grid is drawn at the till's own proportions, so what a manager arranges
   here is what a clerk sees. Selection is the interaction — drag a box, or hold
   Shift — so the panel beside it can act on several buttons at once. */

.sp-bar {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 14px;
  padding: 12px 14px;
  border: 1px solid var(--line, #d7dbe2);
  border-radius: 10px;
  background: var(--panel, #fff);
}
/* `.tbl-bar` above a table is the same furniture as `.sp-bar` above the screen
   editor and borrows its parts by name — but this rule was scoped to `.sp-bar`
   alone, so the spacer in a filter bar spaced nothing. The row count and the
   bulk-edit strip, which the markup puts after the spacer precisely so they
   sit at the far end, were bunched against the Clear button with the rest of
   the bar empty beside them. */
.sp-bar .sp-spacer,
.tbl-bar .sp-spacer { flex: 1 1 auto; }
.sp-pick { display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; margin: 0; }
/* A date field, a text field and a select are three different heights in the
   same browser — Chrome gives `type="date"` 44px for its calendar indicator and
   a plain text box 41px. The bar aligns them on their bottom edge, which is
   what matters, but their *labels* then sit on three different lines: on the
   Sales Explorer "Department" was drawn 3px below "From" and "To". One height
   for every control in a filter bar, which is also the 44px touch target. */
.sp-pick select, .sp-pick input { min-width: 90px; min-height: 44px; }
.sp-bar .check { margin: 0; }

.sp-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 16px;
  align-items: start;
}
@media (max-width: 1100px) { .sp-layout { grid-template-columns: 1fr; } }

.sp-panel { display: flex; flex-direction: column; gap: var(--stack); }

.sp-grid {
  display: grid;
  gap: 6px;
  /* 16:9, the shape of the space a sale grid actually occupies on a till.
     Fixing it here is what stops a 4-row screen drawing keys twice the height
     of a 8-row one and giving a false impression of both. */
  aspect-ratio: 16 / 9;
  padding: 8px;
  border-radius: 12px;
  background: #1b1f27;
  outline: none;
  /* The drag-select gesture: without this the browser starts a text selection
     the moment a pointer moves across the grid. */
  user-select: none;
  touch-action: none;
}
.sp-grid:focus-visible { box-shadow: 0 0 0 3px rgba(165, 199, 21, .5); }

.sp-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  padding: 4px;
  border-radius: 8px;
  border: 1px dashed rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .04);
  color: rgba(255, 255, 255, .35);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.15;
  text-align: center;
  cursor: pointer;
  overflow: hidden;
}
.sp-cell span {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
/* A programmed button: solid, and in the till's default key colour until the
   manager gives it one of its own. */
.sp-cell.filled {
  border-style: solid;
  border-color: rgba(255, 255, 255, .14);
  background: #2b313d;
  color: #fff;
}
/* A space set aside: sized by the manager, not yet given anything to do.
   Deliberately not `.filled` — it is an empty cell that happens to be bigger,
   and drawing it as a key would have a manager hunting for the product on it.
   It draws nothing at all on the till and nothing in Preview, which the
   `:not(.filled)` rule further down already takes care of. */
.sp-cell.reserved {
  border-style: dashed;
  border-color: rgba(165, 199, 21, .5);
  background:
    repeating-linear-gradient(
      135deg,
      rgba(165, 199, 21, .07),
      rgba(165, 199, 21, .07) 7px,
      transparent 7px,
      transparent 14px
    ),
    rgba(255, 255, 255, .03);
  color: rgba(255, 255, 255, .5);
}
.sp-reserved-size {
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  letter-spacing: .04em;
  opacity: .8;
}
.sp-cell.selected {
  outline: 3px solid var(--brand, #a5c715);
  outline-offset: -3px;
}
/* A product or screen that no longer exists. Called out here rather than left
   to be found on a till, which is the whole reason the editor knows about it. */
.sp-cell.missing {
  border-color: #d03227;
  color: #ffd7d2;
  background: repeating-linear-gradient(
    45deg, #4a1f1c, #4a1f1c 6px, #3a1815 6px, #3a1815 12px
  );
}
.sp-arrow { font-style: normal; font-size: 11px; opacity: .8; letter-spacing: 1px; }

.sp-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}
.sp-swatch {
  width: 34px;
  height: 26px;
  border-radius: 6px;
  border: 1px solid var(--line, #d7dbe2);
  cursor: pointer;
  padding: 0;
}
.sp-swatch.on { outline: 3px solid var(--brand, #a5c715); outline-offset: -3px; }

.sp-spans { display: flex; gap: 10px; margin-top: 12px; }
.sp-spans label { flex: 1; font-size: 12.5px; }

/* The screen editor, part two: the states the grid gained when it learned to
   be dragged around rather than only clicked on.

   `caret` is where the keyboard is, which is not the same as what is selected —
   arrow keys walk it, Shift extends from it. `drop` is where a dragged block
   would land, drawn before the drop rather than after it, because a move that
   cannot be previewed is a move that gets undone. */
.sp-cell.caret { box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .55); }
.sp-cell.drop {
  outline: 2px dashed var(--brand, #a5c715);
  outline-offset: -2px;
  opacity: .85;
}
.sp-grid.moving { cursor: grabbing; }
.sp-grid.moving .sp-cell { cursor: grabbing; }

/* Preview: the grid as a clerk meets it, without the editor's empty keys. The
   layout does not move — only the scaffolding stops being drawn — so what is
   being checked is what a till will show. */
.sp-grid.preview .sp-cell:not(.filled) {
  border-color: transparent;
  background: transparent;
}
.sp-grid.preview .sp-cell.selected { outline-color: transparent; }
.sp-grid.preview .sp-cell.caret { box-shadow: none; }

.sp-order { display: flex; gap: 4px; }

/* The line under the grid: what the layout holds, and whether it is saved. */
.sp-status {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 12.5px;
  color: var(--muted, #6b7280);
}
.sp-flag {
  padding: 2px 8px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 11.5px;
}
.sp-flag.ok { background: rgba(33, 167, 62, .14); color: #21a73e; }
.sp-flag.warn { background: rgba(206, 122, 10, .16); color: #b56a08; }
.sp-flag.bad { background: rgba(208, 50, 39, .14); color: #d03227; }

/* The check list. Each row selects the key it names, which is the only reason
   the list is worth having — "row 3, column 5" is not a thing anybody counts
   across a grid by eye. */
.sp-issues { display: flex; flex-direction: column; gap: 6px; }
.sp-issue {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px;
  border: 1px solid var(--line, #d7dbe2);
  border-left: 3px solid #d03227;
  border-radius: 8px;
  background: var(--panel, #fff);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}
.sp-issue:hover { border-color: #d03227; }

/* ---------------------------------------------------------------------------
   Screen programming, part two: the bars, and pictures on keys.

   A bar is a screen — one or two rows of the same buttons — so almost nothing
   here is new machinery. What is new is shape: a bar has to be drawn as the
   strip it is, in the place it sits, or a manager sizes an eleven-key action
   bar as though it were a page of products.
   --------------------------------------------------------------------------- */

/* What the tills wear. First card on the page, because "which screen do my
   tills open on" was being asked by people looking straight at the tick box
   that sets it. */
.sp-defaults { margin-bottom: 12px; padding: 12px 14px; }
.sp-defaults-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}
.sp-defaults-head h3 { margin: 0; }
.sp-defaults-head .sp-flag { margin-left: auto; }
.sp-defaults-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr)) 180px;
  gap: 12px;
  align-items: end;
  margin-top: 10px;
}
@media (max-width: 1100px) {
  .sp-defaults-grid { grid-template-columns: 1fr; }
}
.sp-defaults-grid > label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12.5px;
  margin: 0;
  min-width: 0;
}

/* The payment screen's pair, on their own row under a rule.
   Placed explicitly rather than left to auto-flow: the till drawing lives in
   the fourth column, and a heading allowed to flow would land on top of it. */
.sp-defaults-split {
  grid-column: 1 / 4;
  border-top: 1px solid var(--line, rgba(255, 255, 255, .1));
  padding-top: 10px;
  margin-top: 2px;
}
@media (max-width: 1100px) {
  .sp-defaults-split { grid-column: 1; }
}

/* A drawing of a till, not a list of names. Which of three layouts is the
   bottom one is a question a picture answers and a dropdown does not — and each
   part of it is the way in to editing that part. */
.sp-till {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 5px;
  border-radius: 10px;
  background: #1b1f27;
  border: 1px solid rgba(255, 255, 255, .1);
  /* Down the side of all three rows: the sale screen's choices, the rule, and
     the payment screen's. Centred rather than left to the grid's `align-items:
     end`, which dropped the drawing to the foot of the card and left it
     looking like it belonged to the payment row alone. */
  grid-column: 4;
  grid-row: 1 / span 3;
  align-self: center;
}
@media (max-width: 1100px) {
  .sp-till { grid-column: 1; grid-row: auto; }
}
.sp-till-body { display: flex; gap: 3px; flex: 1 1 auto; min-height: 38px; }
.sp-till-bill {
  width: 34%;
  border-radius: 5px;
  background: repeating-linear-gradient(
    to bottom, rgba(255, 255, 255, .1), rgba(255, 255, 255, .1) 2px,
    transparent 2px, transparent 7px
  );
}
.sp-till [data-part] {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 6px;
  border-radius: 5px;
  background: rgba(255, 255, 255, .06);
  border: 1px dashed rgba(255, 255, 255, .16);
  color: rgba(255, 255, 255, .5);
  font-size: 10.5px;
  font-weight: 700;
  text-align: center;
  line-height: 1.1;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  cursor: pointer;
}
.sp-till-grid { flex: 1 1 auto; }
.sp-till [data-part].custom {
  border-style: solid;
  background: rgba(165, 199, 21, .16);
  border-color: rgba(165, 199, 21, .5);
  color: #cfe86b;
}
.sp-till [data-part].editing { outline: 2px solid var(--brand, #a5c715); outline-offset: 1px; }

/* Which kind of layout. Tabs, because a bar and a sale screen are different
   things that happen to be built the same way. */
/* In the toolbar, so they cost a row's padding rather than a row. */
.sp-surfaces { display: flex; gap: 6px; flex-wrap: wrap; align-self: center; }
.sp-surface {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  border-radius: 999px;
  border: 1px solid var(--line, rgba(0, 0, 0, .14));
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.sp-surface:hover { border-color: var(--brand, #a5c715); }
.sp-surface.on {
  background: var(--brand, #a5c715);
  border-color: var(--brand, #a5c715);
  color: #131a04;
}
/* A tiny picture of the thing each tab holds, so the three read apart at a
   glance rather than by their words. */
.sp-surface-art {
  width: 22px;
  height: 16px;
  border-radius: 3px;
  border: 1px solid currentColor;
  opacity: .65;
  background-repeat: no-repeat;
}
.sp-surface-art.sale {
  background-image:
    linear-gradient(currentColor, currentColor), linear-gradient(currentColor, currentColor),
    linear-gradient(currentColor, currentColor), linear-gradient(currentColor, currentColor);
  background-size: 7px 5px;
  background-position: 2px 2px, 11px 2px, 2px 8px, 11px 8px;
}
.sp-surface-art.top {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 16px 4px;
  background-position: 2px 2px;
}
.sp-surface-art.bottom {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 16px 4px;
  background-position: 2px 8px;
}

/* Just this screen's bars, alongside the venue's rather than under them. */
.sp-perscreen {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  padding-left: 12px;
  border-left: 1px solid var(--line, #d7dbe2);
}
.sp-perscreen[hidden] { display: none; }

/* The stage. A bar laid out in a box on its own is eleven keys in a vacuum;
   laid out under a ghost of the sale screen it is obviously the strip along
   the bottom of a till. */
.sp-stage { display: flex; flex-direction: column; gap: 6px; }
.sp-stage.framed .sp-grid { aspect-ratio: auto; }

/* The till's own fixed key, beside the top bar being laid out.
   `display: contents` everywhere else, so on a sale screen and a bottom bar
   the grid is still a direct flex child of the stage and every rule about how
   it is sized — including the pop-out's — applies exactly as it did. */
.sp-bar-row { display: contents; }
.sp-stage.with-nav .sp-bar-row {
  display: flex;
  align-items: stretch;
  gap: 8px;
  min-height: 0;
}
.sp-stage.with-nav .sp-bar-row > .sp-grid {
  flex: 1 1 auto;
  min-width: 0;
  margin-inline: 0;
}
.sp-fixed-nav {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 8px 10px;
  border-radius: 10px;
  border: 1px dashed rgba(255, 255, 255, .22);
  background: rgba(255, 255, 255, .05);
  cursor: not-allowed;
}
.sp-fixed-nav[hidden] { display: none; }
.sp-fixed-nav-key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 9px;
  border-radius: 8px;
  background: rgba(255, 255, 255, .1);
  color: rgba(255, 255, 255, .9);
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
}
.sp-fixed-nav-icon { color: var(--brand, #a5c715); }
.sp-fixed-nav-caret { opacity: .6; font-size: 10px; }
.sp-fixed-nav-note {
  font-size: 10px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .4);
}

/* A grid taller than the window is one whose bottom row a manager cannot see
   without scrolling the toolbar off — and the toolbar is where Save lives. The
   16:9 shape is kept by giving back width rather than by stretching, so what is
   arranged here is still the shape a clerk meets. */
.sp-stage:not(.framed) .sp-grid {
  max-height: 62vh;
  width: min(100%, calc(62vh * 16 / 9));
  margin-inline: auto;
}
.sp-ghost {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 120px;
  border-radius: 12px;
  border: 1px dashed var(--line, rgba(0, 0, 0, .16));
  background: repeating-linear-gradient(
    -45deg, rgba(127, 127, 127, .05), rgba(127, 127, 127, .05) 8px,
    transparent 8px, transparent 16px
  );
  color: var(--muted, #7a8394);
  font-size: 12px;
  font-style: italic;
}

/* The grid, when it is a bar. Row height fixed rather than derived from an
   aspect ratio: a bar is as tall as a bar, and a 1-row 16-column grid at 16:9
   would draw keys a manager could not read the labels in. */
.sp-grid.bar {
  aspect-ratio: auto;
  grid-auto-rows: 74px;
}

/* The key's face: the picture or emoji, above the words, exactly as the till
   stacks them. */
.sp-face-art {
  display: block;
  flex: 0 0 auto;
  font-size: 20px;
  line-height: 1;
  max-width: 100%;
}
/* A picture *is* the key, not a stamp above its name.
   Absolutely positioned behind everything else in the cell, so the label —
   when the venue has asked for one — letters over it rather than beside it.
   This is the till's own stacking; see `_media` in programmed_grid.dart. */
.sp-face-fill {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
}
.sp-face-fill img {
  display: block;
  width: 100%;
  height: 100%;
  /* object-fit and transform are set per key from spFrameStyle(). The
     transform is `translate(x%, y%) scale(s)`, which CSS applies right to
     left — scaled first, then shifted by a percentage of the *key's* size.
     The till composes the same two steps in the same order, and if the two
     ever disagree the editor's preview is lying about what a clerk will see. */
  transform-origin: center;
}
/* Lettering over a photograph, when the venue has asked for the name as well.
   The scrim is the whole reason this is readable: a white label on a picture
   that happens to be pale is a key nobody can read, and which keys those are
   depends on the photograph rather than on anything the editor knows. */
.sp-over-art {
  position: relative;
  z-index: 1;
  padding: 1px 5px;
  border-radius: 5px;
  background: rgba(12, 15, 20, .62);
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}
/* Borrowed from the product rather than given to the key. Faded so that "this
   key has a picture" and "this key was given one" stay apart — otherwise
   clearing a key's own emoji looks like it did nothing. */
.sp-face-art.inherited { opacity: .5; }
/* Faded a little rather than halved: a photograph at 50% on a dark grid is
   barely a picture at all, and the point of drawing the borrowed one is that a
   manager can see what the key will actually look like. */
.sp-face-fill.inherited { opacity: .72; }

/* ---------------------------------------------------------------------------
   Framing a picture on a key

   Drawn at the key's own proportions — a 2x2 stage is square, a 1x3 is a strip
   — because "does this picture work here" is a question only the real shape
   answers. `aspect-ratio` is set from the grid by spRenderFrame().
   --------------------------------------------------------------------------- */
.sp-frame {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line, rgba(0, 0, 0, .12));
}
.sp-frame-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 12.5px;
  font-weight: 600;
}
.sp-frame-stage {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 10px;
  border: 1px solid var(--line, rgba(0, 0, 0, .16));
  /* The chequerboard shows through wherever the picture does not reach, which
     is what "Whole picture" looks like on a key whose shape does not match it.
     A flat colour here would read as part of the photograph. */
  background:
    repeating-conic-gradient(rgba(128, 128, 128, .18) 0% 25%, transparent 0% 50%)
      50% / 16px 16px;
  cursor: grab;
  touch-action: none;
}
.sp-frame-stage.dragging { cursor: grabbing; }
.sp-frame-stage img {
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: center;
  user-select: none;
  -webkit-user-drag: none;
}
.sp-frame-hint {
  position: absolute;
  left: 50%;
  bottom: 6px;
  transform: translateX(-50%);
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(12, 15, 20, .6);
  color: #fff;
  font-size: 10.5px;
  letter-spacing: .02em;
  pointer-events: none;
  opacity: .85;
  transition: opacity .15s;
}
.sp-frame-stage.dragging .sp-frame-hint,
.sp-frame-stage:hover .sp-frame-hint { opacity: 0; }
.sp-frame-zoom {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  font-size: 12px;
  color: var(--muted, #7a8394);
}
.sp-frame-zoom input[type='range'] { flex: 1 1 auto; min-width: 0; }
.sp-frame-zoom output {
  flex: 0 0 auto;
  min-width: 44px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.sp-frame-presets { display: flex; gap: 6px; margin-top: 8px; }
.sp-frame-presets .btn { flex: 1 1 0; }
.sp-frame-presets .btn.on {
  outline: 2px solid var(--brand, #a5c715);
  outline-offset: -2px;
}
#sp-show-label-wrap { margin-top: 10px; }

/* A live display is not a key. Drawn as a sketch of what the till will put
   there, so a manager sizes the open-tables strip like a strip. */
.sp-cell.widget {
  border-style: solid;
  border-color: rgba(75, 87, 232, .55);
  background: rgba(75, 87, 232, .16);
  color: #cfd4ff;
}
.sp-widget {
  display: flex;
  flex-wrap: nowrap;
  gap: 4px;
  overflow: hidden;
  max-width: 100%;
  font-size: 10px;
  font-style: normal;
}
.sp-widget i { font-style: normal; opacity: .85; white-space: nowrap; }
.sp-widget .sp-chip {
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .12);
}
.sp-widget .sp-chip.on { background: var(--brand, #a5c715); color: #131a04; }
.sp-widget .sp-mono { font-variant-numeric: tabular-nums; font-weight: 700; }

/* The face editor in the inspector. */
.sp-face {
  margin-top: 12px;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid var(--line, rgba(0, 0, 0, .12));
}
.sp-face-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 12.5px;
  font-weight: 700;
  margin-bottom: 8px;
}
.sp-emoji-row { display: flex; flex-direction: column; gap: 8px; }
.sp-emoji-palette { display: flex; flex-wrap: wrap; gap: 4px; }
.sp-emoji-key {
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 7px;
  border: 1px solid transparent;
  background: rgba(127, 127, 127, .1);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}
.sp-emoji-key:hover { border-color: var(--brand, #a5c715); }
.sp-emoji-key.on { border-color: var(--brand, #a5c715); background: rgba(165, 199, 21, .22); }

.sp-image-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.sp-image-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 40px;
  flex: 0 0 auto;
  border-radius: 8px;
  border: 1px solid var(--line, rgba(0, 0, 0, .14));
  background: rgba(127, 127, 127, .08);
  background-size: cover;
  background-position: center;
  font-size: 20px;
}
.sp-image-preview.empty { color: var(--muted, #7a8394); font-size: 13px; }
.sp-image-preview.inherited { opacity: .55; }
.sp-image-actions { display: flex; flex-wrap: wrap; gap: 6px; }

/* Pictures this venue already has, offered as a strip to click rather than a
   path to type. */
.sp-gallery { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
.sp-gallery-key {
  width: 40px;
  height: 30px;
  padding: 0;
  border-radius: 6px;
  border: 1px solid var(--line, rgba(0, 0, 0, .14));
  background-size: cover;
  background-position: center;
  cursor: pointer;
}
.sp-gallery-key:hover { border-color: var(--brand, #a5c715); }
.sp-gallery-key.on { outline: 2px solid var(--brand, #a5c715); outline-offset: 1px; }

/* An advisory about the layout as a whole — "no Pay key on this bar" — rather
   than a key pointing at something deleted. Worth showing, but it is not a
   link and must not look like one. */
.sp-issue.warn { border-color: rgba(206, 122, 10, .5); cursor: default; }
.sp-issue.warn:hover { border-color: rgba(206, 122, 10, .5); }


/* The product picker is a list box now, not a one-line select: a search above
   it and eight rows visible is the difference between choosing a product and
   scrolling past nine hundred of them. */
#sp-product { width: 100%; margin-top: 6px; }
#sp-product-q { width: 100%; }

/* A filter bar above a table. Same furniture as the screen editor's bar, which
   is why it borrows .sp-pick and .sp-spacer rather than growing a second set of
   names for a label above a control. */
.tbl-bar {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border: 1px solid var(--line, #d7dbe2);
  border-radius: 10px;
  background: var(--panel, #fff);
}
.tbl-bar input[type="search"] { min-width: 240px; }

/* Sortable headings. The arrow only appears on the column actually in use —
   three greyed arrows across a header row is decoration, not information. */
table.sortable th[data-sort] {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
table.sortable th[data-sort]:hover { color: var(--ink, #10131a); }
table.sortable th.sorted::after {
  content: '▲';
  font-size: 10px;
  /* An explicit gap, not a space inside the content string. A 10px space
     between an 11px uppercase heading and the arrow is about three pixels, so
     "SUB DEPARTMENT▲" read as one word with a mark stuck on the end of it. */
  margin-left: 5px;
  vertical-align: 1px;
  opacity: .75;
}
table.sortable th.sorted[data-dir="down"]::after { content: '▼'; }

/* A member's face in the customers table.

   Round, small, and always drawn: a venue that has photographed half its
   members needs to see which half at a glance, and an empty cell reads as a
   column that is not working rather than as a member with no photograph. */
.cust-face-cell { width: 44px; }
.cust-face {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  display: grid;
  place-items: center;
  background: var(--surface-2);
}
.cust-face-none {
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .02em;
  border: 1px solid var(--line);
}

/* ---------------------------------------------------------------------------
   The product picker: search a catalogue, tick what belongs, see the answer.

   Used by Mix & Match, where the answer is a set of products out of several
   hundred. The chosen ones sit above the list as chips rather than only as
   ticks inside it, because the question a manager is answering is "what is in
   this deal" and scrolling a list to find three ticks is not an answer.
   --------------------------------------------------------------------------- */
.pp-field { margin-top: 6px; }
.pp-search { width: 100%; }
.pp-chosen {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0 0;
}
.pp-chosen:empty { display: none; }
.pp-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 4px 10px;
  border-radius: 999px;
  background: var(--brand-soft);
  color: var(--brand-deep);
  font-size: 13px;
  font-weight: 600;
}
.pp-x {
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  padding: 2px 4px;
  border-radius: 999px;
}
.pp-x:hover { background: rgba(0, 0, 0, .12); }
.pp-empty { margin: 8px 0 0; }
/* Tall enough to be worth scrolling and short enough to leave the rest of the
   form on screen. A catalogue of four hundred is why it scrolls at all. */
.pp-list {
  list-style: none;
  margin: 10px 0 0;
  padding: 4px;
  max-height: 240px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
}
.pp-row { border-radius: 8px; }
.pp-row:hover { background: var(--hover); }
.pp-row label {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding: 7px 8px;
  cursor: pointer;
}
.pp-row input[type="checkbox"] { width: auto; margin: 0; }
.pp-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pp-meta { white-space: nowrap; }

/* ---------------------------------------------------------------------------
   The ordered modifier picker on the product form.

   A numbered list, deliberately: the order is the meaning — "single or double"
   is asked before "which mixer" — so the numbers are the field's own label for
   what it does, not decoration.
   --------------------------------------------------------------------------- */
.mod-field { margin-top: 6px; }
.mod-list {
  margin: 0 0 8px;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.mod-list:empty { display: none; }
.mod-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.mod-name { font-weight: 500; }
/* Pushes the keys to the right whatever the name's length. */
.mod-keys { margin-left: auto; display: flex; gap: 4px; }
.mod-add { display: flex; gap: 8px; align-items: center; }
.mod-add select { flex: 1; }

/* ---------------------------------------------------------------------------
   Picking several products at once, and editing a cell where it sits.
   --------------------------------------------------------------------------- */
/* Shift-clicking two boxes in a table selects every word between them as
   text as well as every row, and the blue survives the re-render. The gesture
   is a selection of rows, so the column does not take part in the other one. */
.pick-col { width: 34px; user-select: none; }
/* `input, select { width: 100% }` near the top of this file is a bare element
   selector, so it reaches every control in the application including the tick
   boxes — and a checkbox is not a field that wants filling. It went unnoticed
   only because the column above is 34px wide and a 34px checkbox still looks
   like a checkbox. Give the same box the full width of a card on a phone and
   it is drawn adrift in the middle of a 650px row, which is where this was
   found. A checkbox is its own size, wherever it is put. */
.pick-col input[type="checkbox"] { width: auto; margin: 0; }
.bulk-bar {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 12px;
  padding: 4px 4px 4px 10px;
  border-radius: 999px;
  background: var(--brand-soft, rgba(94, 53, 177, 0.08));
}
.bulk-count { font-size: 13px; font-weight: 600; }

/* An inline cell reads as text until it is being changed, so a table of two
   hundred products does not look like a form. */
.cell-edit {
  width: 100%;
  border: 1px solid transparent;
  /* `background-color`, never the shorthand — see `select.cell-edit` below,
     where a shorthand here was half of a bug that only showed up in Night. */
  background-color: transparent;
  background-image: none;
  border-radius: 6px;
  padding: 3px 6px;
  font: inherit;
  color: inherit;
}
/* A number is not a sentence.

   `input, select { width: 100% }` hands every inline editor the browser's
   default field width as its *intrinsic* size, and a table sized to its
   content then asks for that: the price column wanted 208px to show "3.00",
   and the catalogue came out 1303px wide inside a card 776px across on an iPad
   in portrait. Half the width of that table was empty boxes. */
.cell-edit[type="number"] { max-width: 96px; }
.cell-edit:hover { border-color: var(--line); }
.cell-edit:focus {
  border-color: var(--brand-deep);
  /* `background-color`, not the shorthand: a shorthand here would throw away
     the chevron a select cell paints as a background image, and it would do it
     only while the cell had focus. See the note under `select.cell-edit`. */
  background-color: var(--card, #fff);
  outline: none;
}
.cell-edit.saving { opacity: 0.5; }
.cell-edit.saved { border-color: #2e7d32; }

/* An inline dropdown, and the chevron that goes with it.
   ---------------------------------------------------------------------------
   THE BUG THIS FIXES, BECAUSE IT WAS INVISIBLE IN DAY MODE

   The venue reported "arrows overlapping the departments and sub departments
   in the product view page". Measured in light mode at six widths, nothing
   overlapped: `.cell-edit`'s `background: transparent` is a SHORTHAND, so it
   cleared the `background-image`, `background-position` and `background-size`
   the generic `select` rule sets, and a class beats an element selector — no
   chevron was drawn at all.

   The dark rules further down set `background-image` ALONE, on a selector
   (`:root[data-theme="dark"] select`) that outranks `.cell-edit`. So in Night
   the image came back while the position and size stayed at their initial
   values — `0% 0%` and `auto` — and a 17px chevron was painted over the first
   letters of "Beers", tiled across the box. Only ever visible on the venue's
   own screen, which is the one in the dark.

   Set as longhands here, on both the class and the theme, so neither can be
   half-overridden again. And the arrow is kept rather than removed: a cell
   that changes a department when you click it should look like it will. */
select.cell-edit {
  cursor: pointer;
  /* Room for the arrow, so the longest sub department name stops short of it
     rather than running underneath. */
  padding-right: 22px;
  text-overflow: ellipsis;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2378717f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 4px center;
  background-size: 13px;
}
:root[data-theme="dark"] select.cell-edit {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a2ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 4px center;
  background-size: 13px;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select.cell-edit {
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a2ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 4px center;
    background-size: 13px;
  }
}

/* ---------------------------------------------------------------------------
   The screen editor's search palette — double-click a key.
   --------------------------------------------------------------------------- */
.sp-palette-back {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(15, 15, 20, 0.45);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 10vh;
}
.sp-palette {
  width: min(620px, 92vw);
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--card, #fff);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
  overflow: hidden;
}
.sp-palette-q {
  border: 0;
  border-bottom: 1px solid var(--line);
  padding: 14px 16px;
  font-size: 16px;
  outline: none;
  background: transparent;
  color: inherit;
}
.sp-palette-list {
  list-style: none;
  margin: 0;
  padding: 6px;
  overflow-y: auto;
  flex: 1;
}
.sp-palette-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
}
.sp-palette-row.on { background: var(--brand-soft, rgba(94, 53, 177, 0.1)); }
.sp-palette-kind {
  flex: 0 0 82px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  opacity: 0.65;
}
.sp-palette-label { font-weight: 600; }
.sp-palette-note { margin-left: auto; }
.sp-palette-empty { padding: 14px; }
.sp-palette-hint { margin: 0; padding: 8px 14px; border-top: 1px solid var(--line); }

/* ---------------------------------------------------------------------------
   The editor in a window of its own (?popup=1).

   Same page, same editor, without the furniture: no sidebar, no page heading,
   no explanation. The point is that the grid, the inspector and the till
   preview all fit at once — the manager was scrolling up to change a key and
   down to see what it did.
   --------------------------------------------------------------------------- */
body.sp-popup .rail,
body.sp-popup .topbar,
body.sp-popup #view-screens .page-head > div:first-child { display: none !important; }

body.sp-popup main { padding: 0; }

body.sp-popup #view-screens {
  height: 100vh;
  display: flex;
  flex-direction: column;
  padding: 10px 12px;
  gap: 8px;
  overflow: hidden;
}

/* The action row keeps its natural height; everything below shares what is
   left and scrolls inside itself rather than pushing the page taller. */
body.sp-popup #view-screens .page-head {
  flex: 0 0 auto;
  margin: 0;
  justify-content: flex-end;
}

/* The defaults card is a setup step, not part of laying a screen out - it is
   the first thing to give up its height in a window sized for the grid. */
body.sp-popup .sp-defaults { display: none; }

body.sp-popup .sp-layout {
  flex: 1 1 auto;
  min-height: 0;
  align-items: stretch;
}
body.sp-popup .sp-layout > * { min-height: 0; }

/* The inspector is a list of controls and is allowed to scroll — it is read
   top to bottom and the manager knows there is more of it. */
body.sp-popup .sp-panel { overflow-y: auto; }

/* The grid is not, and making it fit is most of why this window exists.

   In the page, the grid is width-driven: it takes the column it is in and its
   16:9 shape decides how tall it comes out. In a window sized for it that is
   backwards — a wide window makes a tall grid, the bottom row goes under the
   fold, and the manager is scrolling again in the window that existed to stop
   them scrolling.

   So in the pop-out the stage takes whatever height is left over and the grid
   is height-driven instead: `height: 100%` with the aspect ratio deciding the
   width, capped at the column so a short wide window gives back height rather
   than overflowing sideways. The shape is the same either way, which is the
   whole point — what is arranged here is the shape a clerk meets. */
body.sp-popup .sp-layout > div:first-child {
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow: hidden;
}
body.sp-popup .sp-stage {
  flex: 1 1 auto;
  min-height: 0;
}
body.sp-popup .sp-stage:not(.framed) .sp-grid {
  max-height: none;
  height: 100%;
  width: auto;
  max-width: 100%;
  margin-inline: auto;
}
/* A bar has a fixed row height, so it cannot be stretched to fill — it keeps
   its own size and the leftover height goes to the ghost of the sale screen
   above or below it, which is what that ghost is for.

   Which means the ghost has to be able to *take* it, and to give it back: at
   120px fixed it is the thing that pushes an eleven-key bar off the bottom of a
   short window. It shrinks to 60 and grows to whatever is left. */
body.sp-popup .sp-stage.framed { justify-content: center; }
body.sp-popup .sp-ghost {
  flex: 1 1 auto;
  min-height: 60px;
}
body.sp-popup .sp-status { flex: 0 0 auto; }


/* ---------------------------------------------------------------------------
   The corner handle

   Bottom-right, on the selected key only, and drawn half outside its own
   corner so there is something to grab on a key one cell wide. It is a real
   element rather than a `resize` CSS property because the grid is cells and
   not pixels: what is being dragged is a span, and it snaps.

   The touch target is deliberately larger than the mark. A manager laying out
   a screen on a Windows tablet is doing this with a thumb, and a 10px square
   is not a thing a thumb can hit.
   --------------------------------------------------------------------------- */
.sp-handle {
  position: absolute;
  right: -5px;
  bottom: -5px;
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 2px solid #12151b;
  background: var(--brand, #a5c715);
  cursor: nwse-resize;
  z-index: 3;
}
/* The hit area, not the mark. Invisible, and twice the size. */
.sp-handle::after {
  content: '';
  position: absolute;
  inset: -10px;
}
/* The cell has to be a containing block for the handle to sit in its corner.
   `position: relative` on every cell rather than only on the selected one, so
   selecting a key does not reflow it.

   And the selected one gives up `overflow: hidden`, which is load-bearing
   rather than tidy: the handle and the invisible square around it both sit
   *outside* the key's corner so a one-cell-wide key still has something to
   grab, and a clipped handle is one that cannot be pressed at all. The label
   inside does its own clipping (see `.sp-cell span`), so nothing else escapes. */
.sp-cell { position: relative; }
.sp-cell.selected { overflow: visible; }
.sp-grid.resizing,
.sp-grid.resizing .sp-cell { cursor: nwse-resize; }

/* ---------------------------------------------------------------------------
   The colour wheel, and the box beside it

   Neither half is enough alone: the wheel hides the value a venue needs to
   read back, and the box is unusable for anybody choosing rather than pasting.
   --------------------------------------------------------------------------- */
.sp-wheel-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
/* Its own width, not a share of the row: the hex box beside it is the half
   that should give, and a wheel narrower than its own swatch is not usable. */
.sp-wheel {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 12px;
  color: var(--muted, #7a8394);
  white-space: nowrap;
}
.sp-wheel input[type='color'] {
  width: 34px;
  height: 30px;
  padding: 2px;
  border: 1px solid var(--line, rgba(0, 0, 0, .16));
  border-radius: 8px;
  background: none;
  cursor: pointer;
}
.sp-hex {
  flex: 1 1 auto;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-transform: lowercase;
}

/* ---------------------------------------------------------------------------
   The type controls, and the venue's fonts
   --------------------------------------------------------------------------- */
.sp-type {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 88px;
  gap: 8px;
  margin-top: 10px;
}
.sp-type label { margin: 0; }

.sp-font-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
}
.sp-font-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid var(--line, rgba(0, 0, 0, .16));
  border-radius: 8px;
}
/* The name, lettered in the font it names. A list of font names all set in the
   same face is a list that cannot answer the only question being asked of it. */
.sp-font-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 17px;
}
.sp-font-row .btn { flex: 0 0 auto; }

/* ---------------------------------------------------------------------------
   Touch

   Not "mobile". `pointer: coarse` is the question actually being asked — is the
   thing pressing this a finger — and it is true of an iPad in landscape at
   1366px, which every width-based breakpoint in this file treats as a desktop.
   That is why the back office felt wrong on a tablet while measuring fine on a
   laptop at the same width.
   --------------------------------------------------------------------------- */
@media (pointer: coarse) {
  /* Apple's own floor is 44pt. A `.btn.small` was 29px tall, and a row with
     Edit / Duplicate / Delete in it was three 29px targets 6px apart — which is
     a mis-tap waiting to happen, and one of them deletes a product. */
  .btn { min-height: 42px; }
  .btn.small { min-height: 38px; padding: 8px 14px; font-size: 13.5px; }
  .nav { min-height: 44px; }
  .nav-group { min-height: 38px; }

  /* THE ONE THAT MAKES A TABLET FEEL BROKEN.

     iOS Safari zooms the whole page in when a form control smaller than 16px
     takes focus, and it does not zoom back out. Tap the product search, and the
     back office is suddenly 1.2x too big with the rail off the left edge — and
     nothing on screen explains why. Every control in here was 14px.

     16px is not a design choice here, it is the threshold. */
  input, select, textarea, .cell-edit {
    font-size: 16px;
    min-height: 42px;
  }
  /* Inline table editing keeps its own smaller frame, but not its own font
     size — see above. */
  .cell-edit { min-height: 36px; }

  /* A checkbox at 13px is not something a thumb can hit, and the pick column of
     the products table is nothing but checkboxes. */
  input[type="checkbox"], input[type="radio"] {
    width: 22px;
    height: 22px;
  }

  /* Momentum scrolling inside the boxes that now scroll rather than the page. */
  .card, #canvas, .sp-panel { -webkit-overflow-scrolling: touch; }
}

/* ---------------------------------------------------------------------------
   Tablets: the gap between the phone drawer and the desktop rail

   The off-canvas drawer stops at 960px and the desktop layout starts at 961px
   with a 236px rail — so an iPad in portrait (1024px) keeps the full rail and
   gets 788px of page, and in landscape (1366px) it gets 1130px. Neither is
   wrong, but both are tighter than the desktop this was laid out on, and the
   two places that assumed otherwise are here.
   --------------------------------------------------------------------------- */
@media (min-width: 961px) and (max-width: 1400px) {
  /* The rail gives back 32px. It is a list of one-line labels and none of them
     needs 236px. */
  .rail { width: 204px; }
  main { padding: 26px 22px 56px; }

  /* Two-up panels are a desktop idea. At this width each half is ~450px and a
     chart in one of them is unreadable. */
  .grid-2 { grid-template-columns: 1fr; }
}

/* ---------- The editor, handed to a window of its own ----------

   Screen programming opens in its own window by default, and the tab it was
   opened from draws a card saying so. Everything the editor is made of is
   hidden rather than removed: coming back to the tab is a class change, not a
   reload, so a layout part-way through is still where it was left. */
body.sp-handed-over #view-screens .sp-defaults,
body.sp-handed-over #view-screens .sp-layout,
body.sp-handed-over #view-screens .page-head .denom-actions > :not(#sp-popout) {
  display: none;
}

.sp-handover {
  max-width: 640px;
  padding: var(--card-pad);
}
.sp-handover h3 { margin: 0 0 8px; }

/* ---------------------------------------------------------------------------
   Night
   ---------------------------------------------------------------------------
   The whole theme is a list of colours because everything above it goes
   through the tokens. Two rules govern what is *not* in here:

     * A picture of a thing keeps its own colours. The receipt preview is a
       photograph of paper and stays white in both themes; the till preview and
       the screen editor's grid are pictures of a till, which was always dark.
       Inverting either would show a manager something no customer will ever
       see, which is the one job those panels have.

     * The rail does not move. It was already near-black, and it is the one
       part of the page that is the same in both themes -- which is what stops
       a switch reading as a different application.

   Applied by `data-theme` on <html> rather than by `prefers-color-scheme`
   alone, because a venue's office and its bar are not the same room and the
   manager gets the choice. The default follows the system; see the media query
   below it, which is guarded so an explicit "light" always wins.
*/
:root[data-theme="dark"] {
  --rail: #111111;
  --rail-text: #9c9c9a;
  --rail-active: #ffffff;
  --rail-line: rgba(255, 255, 255, .07);
  --rail-hover: rgba(255, 255, 255, .05);
  --rail-group: #5f596b;
  --rail-scroll: rgba(255, 255, 255, .18);

  --bg: #0d1014;
  --card: #151a20;
  --panel: #151a20;
  --surface-2: #1b222a;
  --input-bg: #10151a;
  --hover: #1b222a;
  --track: #262f39;
  --line: #262f39;
  --text: #e8edf1;
  --muted: #94a2ae;

  /* Lime on near-black reads well as a fill and badly as small text, so the
     deep variant moves the other way in the dark: it has to be *lighter* than
     the brand here, not darker. */
  --brand-soft: #26310a;
  --brand-deep: #b7db2a;

  --blue: #6ba4ff;
  --amber: #ffb648;
  --red: #ff6b6f;
  --green: #46c78c;

  /* On near-black the brand lime is the colour that sings, so chart 1 is the
     lime itself rather than the deepened version the light theme needs. The
     rest are lifted the same way every other colour in this block is. */
  --chart-1: #a5c715;
  --chart-2: #6ba4ff;
  --chart-3: #ffb648;
  --chart-4: #2fc9b5;
  --chart-5: #a78bfa;
  --chart-6: #ff6b6f;
  --chart-7: #94a2ae;
  --chart-8: #d1a34a;

  --ok-wash: #10281c;
  --warn-wash: #2e2110;
  --warn-ink: #ffb648;
  --bad-wash: #2e1517;
  --flash: #2f2712;

  /* A shadow the colour of the page beneath it. The light theme's near-black
     shadow is invisible on a dark canvas, so cards would lose their edges
     entirely; this is deeper and relies on the border to do most of the work. */
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 6px 20px rgba(0, 0, 0, .3);
}

/* The default, for anybody who has never touched the switch. Guarded on
   `:not([data-theme="light"])` so an explicit choice of Day survives a laptop
   set to dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --rail: #111111;
    --rail-text: #9c9c9a;
    --rail-active: #ffffff;
    --rail-line: rgba(255, 255, 255, .07);
    --rail-hover: rgba(255, 255, 255, .05);
    --rail-group: #5f596b;
    --rail-scroll: rgba(255, 255, 255, .18);

    --bg: #0d1014;
    --card: #151a20;
    --panel: #151a20;
    --surface-2: #1b222a;
    --input-bg: #10151a;
    --hover: #1b222a;
    --track: #262f39;
    --line: #262f39;
    --text: #e8edf1;
    --muted: #94a2ae;
    --brand-soft: #26310a;
    --brand-deep: #b7db2a;
    --blue: #6ba4ff;
    --amber: #ffb648;
    --red: #ff6b6f;
    --green: #46c78c;
    --chart-1: #a5c715;
    --chart-2: #6ba4ff;
    --chart-3: #ffb648;
    --chart-4: #2fc9b5;
    --chart-5: #a78bfa;
    --chart-6: #ff6b6f;
    --chart-7: #94a2ae;
    --chart-8: #d1a34a;
    --ok-wash: #10281c;
    --warn-wash: #2e2110;
    --warn-ink: #ffb648;
    --bad-wash: #2e1517;
    --flash: #2f2712;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 6px 20px rgba(0, 0, 0, .3);
  }
}

/* The receipt designer's paper. A preview of something printed on white paper
   in black ink, so it is stated rather than inherited -- in either theme. */
:root[data-theme="dark"] .rd-paper,
:root[data-theme="dark"] .rd-paper * {
  color: #000;
}

/* Two controls the browser draws itself. Without this the dark theme gets a
   white scrollbar down the side of every table and a light date picker. */
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

/* The switch itself: a two-key segmented control in the rail's footer, beside
   the online dot. Small, because it is set once and then never touched. */
.theme-switch {
  display: inline-flex;
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 8px;
  overflow: hidden;
}
.theme-switch button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--rail-text);
  font: inherit;
  font-size: 12px;
  padding: 4px 10px;
  cursor: pointer;
}
.theme-switch button + button { border-left: 1px solid rgba(255, 255, 255, .14); }
.theme-switch button:hover { color: var(--rail-active); }
.theme-switch button[aria-pressed="true"] {
  background: var(--brand);
  color: var(--on-brand);
}

.rail-theme { padding: 12px 22px 20px; }

/* The bulk-fill product picker.
   ----------------------------------------------------------------------------
   A department is the fast way to fill a screen and was, for a while, the only
   way: it dropped in everything on the shelf and a manager who wanted eight of
   the fourteen lagers deleted six keys afterwards. The list below is the answer,
   so it has to stay a *list* — scrolling inside its own box rather than pushing
   "Fill the selection" off the bottom of a panel that is already tall. */
.sp-fill-picker { margin-top: 10px; }

.sp-fill-tools {
  display: flex;
  gap: 6px;
  align-items: center;
  margin-bottom: 6px;
}

.sp-fill-tools input {
  flex: 1 1 auto;
  min-width: 0;
}

.sp-fill-list {
  max-height: 210px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px;
  background: var(--card);
}

.sp-fill-list .check {
  margin: 0;
  padding: 4px 6px;
  border-radius: 6px;
  font-size: 12.5px;
  gap: 8px;
}

.sp-fill-list .check:hover { background: rgba(127, 127, 127, .1); }

/* The price sits at the end of the row, greyed, so a manager can tell two
   similarly named products apart without opening the catalogue. */
.sp-fill-list .sp-fill-price {
  margin-left: auto;
  opacity: .6;
  font-variant-numeric: tabular-nums;
}

.sp-fill-list .sp-fill-empty {
  padding: 10px 6px;
  opacity: .65;
  font-size: 12.5px;
}

/* The catalogue import.
   ----------------------------------------------------------------------------
   Two cards side by side, because the order is the instruction: get the
   template, then upload the file. They stack on a narrow screen rather than
   shrinking, since a file input has a minimum useful width and a squeezed one
   is a button nobody can read. */
.import-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--stack);
}

.import-grid input[type="file"] {
  width: 100%;
  font-size: 13px;
}

.import-summary {
  margin: 0;
  padding-left: 20px;
  line-height: 1.9;
}

/* Errors are a list somebody works down with the spreadsheet open beside them,
   so the row numbers line up and the list scrolls rather than running the page
   off the bottom. */
.import-errors {
  margin: 6px 0 0;
  padding-left: 20px;
  max-height: 260px;
  overflow-y: auto;
  font-size: 13px;
  line-height: 1.8;
}

#import-problems .error { margin: 14px 0 0; }

/* Reports.
   ----------------------------------------------------------------------------
   The screen and the PDF are the same document in two media, and they are held
   in step deliberately: the dark band, the lime rule under it, the four meta
   cells and the six figure tiles are the same furniture in the same order as
   vesopa_server/src/reports.js draws on paper. Somebody who ran a report on
   screen and then opens the file they exported should not have to work out
   whether it is the same report. */

/* Visible to a screen reader, gone from the page. The export format select
   needs a label; the word "Format" beside a dropdown that says PDF does not
   earn its width. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* The fields, then the actions, in two rows.
   These were one flex line with the actions pushed over by `margin-left:auto`.
   That works at 1400px and falls apart below it: the buttons wrap to their own
   line anyway but without the rule above them, and the gap left behind sits in
   the middle of the fields. A grid gives every field a whole column and drops
   to one column on a phone, where a squeezed date input is one nobody can read
   the year in. */
.rr-controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px 14px;
  align-items: end;
}

.rr-controls label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  margin: 0;
  min-width: 0;
}

.rr-controls select,
.rr-controls input {
  width: 100%;
  min-width: 0;
}

.rr-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* Format, view and download belong together — they are one sentence: "this
   format, look at it or save it". Kept in their own box so a wrap moves all
   three, rather than leaving a lone dropdown under Run report looking like a
   fourth filter. */
.rr-export-group {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-left: auto;
}

/* The label around the format select carries only a screen-reader name, so it
   has no visible text to make room for — but it was still being laid out as a
   block with a line box in it, which made it 44px against the buttons' 40 and
   dropped the select six pixels below Run report, View PDF and Download.
   Measured at 280-320 against 274-314.

   A flex box takes its height from its child, so the label becomes exactly as
   tall as the select inside it and all four controls share one line. */
/* `.rd-card label` — one class and one type — outranks a single class, and the
   report panel carries `rd-card`. So this rule was losing to a rule written for
   a different card entirely: the label stayed `display: block` with 10px under
   it, which is where the group's extra height came from and why the select sat
   five pixels above the three buttons beside it. Named through the card it is
   in, so it wins on specificity rather than on being written later. */
.rd-card .rr-format-field,
.rr-format-field {
  display: flex;
  align-items: center;
  margin: 0;
}
.rr-format-field select {
  min-width: 92px;
  /* Both, because `min-height: 44px` is set on every select further down for
     touch and a min-height always beats a height — so stating the height alone
     left this one 44px tall among 40px buttons, which is the mismatch this rule
     exists to remove. */
  height: var(--control-h);
  min-height: var(--control-h);
  /* And no margin. `input, select, textarea` gives every control `margin-top:
     6px` so that it clears the word above it — but this label has no visible
     word, only a screen-reader one, so the six pixels were pure height: the
     label came out 46px against 40px buttons and the select sat three pixels
     high of the line. Measured on the live site at 262-302 against 265-305. */
  margin: 0;
}

@media (max-width: 720px) {
  /* Nothing is pushed right on a phone: `margin-left:auto` there means the
     buttons are two thirds of the way across a 360px screen with a hole beside
     them. Full-width, in the order they are used. */
  .rr-export-group { margin-left: 0; width: 100%; }
  .rr-actions > .btn { width: 100%; }
  .rr-export-group .btn { flex: 1 1 120px; }
  .rr-format-field { flex: 1 1 100%; }
  .rr-format-field select { width: 100%; }
}

/* ---- The report's own header ---------------------------------------------
   The same band the PDF opens with, so the screen and the file are recognisably
   one document. Stated in absolute colours rather than in theme tokens: this is
   the brand's near-black with the brand's lime on it, in both themes, exactly
   as it prints. */
.rr-head {
  background: #111111;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  margin-bottom: var(--stack);
}

.rr-head-band {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 14px;
  padding: 22px var(--card-pad) 20px;
  border-bottom: 4px solid var(--brand);
}

.rr-head-logo {
  display: block;
  width: 100%;
  max-width: 175px;
  height: auto;
  margin-bottom: 14px;
}
.rr-head h3 { color: #fff; font-size: 21px; margin: 0; }
.rr-head-kicker {
  margin: 4px 0 0;
  font-size: 10.5px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: #b8bcb0;
}
.rr-head-site {
  text-align: right;
  min-width: 0;
  flex: 1 1 220px;
}
.rr-head-site strong { color: var(--brand); font-size: 15px; }
.rr-head-site span { display: block; color: #b8bcb0; font-size: 12px; margin-top: 3px; }

/* The four things every export prints in its header, in the same order. */
.rr-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px;
  background: rgba(255, 255, 255, .09);
}
.rr-meta > div { background: #111111; padding: 14px var(--card-pad); min-width: 0; }
.rr-meta dt {
  font-size: 9.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #8f948a;
  margin: 0 0 5px;
  font-weight: 700;
}
.rr-meta dd {
  margin: 0;
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* ---- The headline figures ------------------------------------------------
   Six of them, three across, the first one lime — the same tiles the PDF puts
   under its meta strip and the same order, because a manager reading the screen
   and an accountant reading the file quote these at each other. */
.rr-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px;
  margin-bottom: var(--stack);
}

.rr-tile {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px 18px 14px;
  min-width: 0;
}
.rr-tile.hero { background: var(--brand); border-color: var(--brand); }
.rr-tile-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}
.rr-tile.hero .rr-tile-label,
.rr-tile.hero .rr-tile-hint { color: rgba(16, 19, 10, .66); }
.rr-tile-value {
  font-size: 25px;
  font-weight: 700;
  letter-spacing: -.02em;
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
.rr-tile.hero .rr-tile-value { color: var(--on-brand); }
.rr-tile-hint { font-size: 11.5px; color: var(--muted); margin-top: 3px; }

/* The heading tick, the same lime mark the PDF puts beside a section title. */
.rr-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
.rr-section-head h3 {
  margin: 0;
  font-size: 15px;
  padding-left: 11px;
  border-left: 3.5px solid var(--brand);
  line-height: 1.25;
}

/* A report has seven columns and a venue may be reading it on a laptop. The
   table scrolls inside its own card rather than pushing the page sideways —
   a horizontally scrolling page loses the left-hand column, which is the one
   that says which department the row is. */
.rr-scroll { overflow-x: auto; }

.rr-table { min-width: 640px; }

.rr-table thead th {
  background: var(--brand-soft);
  color: var(--brand-deep);
  font-size: 10.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  border-bottom: 0;
}

/* Figures right-aligned and tabular, so the decimal points line up down the
   column. A money column that does not is a money column nobody can scan. */
.rr-table td.num,
.rr-table th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.rr-table tbody tr:nth-child(even) { background: var(--surface-2); }

/* The zebra out-specifies the shared `tbody tr:hover`, so without this the
   pointer stops responding on every other row. */
.rr-table tbody tr:hover,
.rr-table tbody tr:nth-child(even):hover { background: var(--hover); }

/* The solid total bar the PDF prints, in the brand near-black rather than in
   `--ink` — which is `--text`, and in the dark theme that is a near-white bar
   with white type on it. `--rail` is #111 in both themes, which is the point
   of it. */
.rr-table tfoot td {
  font-weight: 700;
  background: var(--rail);
  color: #fff;
  border-top: 0;
}
.rr-table tfoot td:first-child { border-radius: 0 0 0 8px; }
.rr-table tfoot td:last-child { border-radius: 0 0 8px 0; }

/* ---- Row actions ---------------------------------------------------------
   A scheduled report has six things you can do to it, and six labelled buttons
   in one cell is a row that is wider than the screen. Icons with a tooltip and
   an accessible name, wrapping in place, in one predictable order: look, save,
   send, history, edit, delete. */
.row-actions {
  display: flex;
  /* NOT `wrap`.

     Nine columns and six buttons is wider than `min-width: 900px` allows, so
     the table layout gave this cell about the width of one icon and the six
     of them wrapped into a vertical stack — a 234px column of buttons holding
     a 250px-tall row open for one line of text, with `vertical-align: middle`
     centring the other eight cells in the middle of all that emptiness. It
     read as a table with one enormous blank row in it.

     A row of actions is one line. Held to it, the cell's min-content width is
     the six buttons, the table asks for the width it actually needs, and the
     card scrolls — which is what the card is for. */
  flex-wrap: nowrap;
  gap: 6px;
  justify-content: flex-end;
}
/* And they keep their size while it happens: `width: 34px` is not a floor on
   its own, because a flex item may still shrink below its width. */
.row-actions .icon-btn { flex: none; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--card);
  color: var(--text);
  cursor: pointer;
}
.icon-btn svg { width: 16px; height: 16px; display: block; }
.icon-btn:hover { background: var(--brand-soft); border-color: var(--brand); }
.icon-btn:disabled { opacity: .45; cursor: default; }
.icon-btn.danger { color: var(--red); }
.icon-btn.danger:hover { background: var(--bad-wash); border-color: var(--red); }

@media (pointer: coarse) {
  /* Same floor as .btn.small below — a 34px target is a mis-tap on a tablet. */
  .icon-btn { width: 40px; height: 40px; }
}

.rs-table td { vertical-align: middle; }
.rs-table { min-width: 900px; }

/* ---- A wide table on a phone ---------------------------------------------
   Scrolling a table sideways on a 360px screen is the worst of both: the
   columns hold it open, whatever is at the right — which on every table here
   is the row's own buttons — is off screen, and finding it means scrolling
   past the column that says which row you are on. On Departments and
   Modifiers that put Edit and Delete out of reach entirely.

   So a table carrying `.table-cards` stops being a table below 760px and each
   row becomes a card. The heading row goes and every cell carries its own
   heading instead, out of the `data-label` the row was rendered with — which
   is why that attribute is not decoration, and why a cell without one shows
   its value alone rather than an empty label.

   Applied to the scheduled reports table in the markup, and by loadCrud to
   every programming table it draws, so this is written once for all of them.

   Kept off the product catalogue and the report tables on purpose: those are
   read as a grid, column against column, and stacking them loses the very
   comparison they exist for. They still scroll. */
@media (max-width: 760px) {
  .table-cards,
  .table-cards tbody,
  .table-cards tr,
  .table-cards td {
    display: block;
    width: auto;
  }

  /* The headings live on the cells now. */
  .table-cards thead { display: none; }

  /* Full width, said loudly enough to be heard.
     `.card > table` sets `width: max-content` for the desktop table and, being
     a class *and* an element, it out-specifies a bare `.table-cards` — so the
     table went on shrink-wrapping to its widest label/value pair and every card
     had a 129px hole down its right-hand side, which is what all that "padding"
     on the right actually was. Matching its shape here is what wins. */
  .card > .table-cards,
  .table-cards {
    width: 100%;
    min-width: 0;
    max-width: 100%;
  }

  .table-cards tr {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 14px 16px;
    margin-bottom: 12px;
    background: var(--card);
  }
  .table-cards tr:last-child { margin-bottom: 0; }
  /* The shared `tbody tr:hover` wash makes a card look pressed on a touch
     screen, where there is no pointer to explain it. */
  .table-cards tbody tr:hover { background: var(--card); }

  .table-cards td {
    border: 0;
    padding: 5px 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    font-size: 13.5px;
    min-width: 0;
    /* The narrow-screen block above sets `nowrap` on every cell, which is
       right for a scrolling table and wrong for a card: a long department name
       would run out of the side of it. */
    white-space: normal;
    overflow-wrap: anywhere;
    /* THE HOLE DOWN THE RIGHT-HAND SIDE OF EVERY CARD.

       `.card > table td { max-width: 42ch }` is a ceiling on a *column*: it
       stops one long note making a desktop table two thousand pixels wide.
       Here there are no columns. Every cell is a full-width row of the card,
       and 42ch is about 357px — so on anything from a large phone to a tablet
       in portrait each row stopped dead at 357px and the rest of the card, up
       to 335px of it, was empty. Products, Stock, Staff, Customers, Vouchers,
       Promotions, Gift cards, Deposits, Departments, Tax, Offices, Billing:
       every table this layout touches, which is why it read as a whole app
       with a margin down one side rather than as one broken page.

       It survived the rule above it because `.card > table td` is a class and
       two elements and `.table-cards td` is a class and one, so the ceiling
       out-specified the layout meant to replace it, and saying `max-width: none`
       on `.table-cards td` alone loses the same argument for the same reason.
       Lifted in the rule below, which carries a class more than the ceiling
       does and so actually reaches it. */
  }

  /* (0,2,1) against the ceiling's (0,1,2) — this is the rule that lifts it.
     The bare `.table-cards td` is for the tables drawn into a plain <div>
     rather than straight into a card, on the kitchen screens: no `.card >`
     ancestor, so no ceiling to lift, but no reason to differ either. */
  .card > .table-cards td,
  .table-cards td { max-width: none; }
  .table-cards td::before {
    content: attr(data-label);
    flex: none;
    /* `justify-content: space-between` spaces *every* item, which is right for
       a cell holding one value and wrong for one holding two: the Printing
       cell put its routing badge in the middle of the card and "Not on
       receipt" against the far edge, as though they were unrelated. An auto
       margin takes the free space before the split does, so the heading goes
       left and everything the cell actually contains stays together on the
       right, however many pieces it comes in. */
    margin-right: auto;
    color: var(--muted);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    white-space: nowrap;
  }
  /* A cell nobody gave a heading to — a computed column, an empty spacer —
     reads better with no label than with a blank one. */
  .table-cards td:not([data-label])::before,
  .table-cards td[data-label=""]::before { content: none; }

  /* `.right` aligns a money column under its heading — a desktop idea, and it
     needs a column to mean anything. In a card it only pushed the heading to
     the far edge, so Price alone read right-to-left while Name, Department and
     Sub Department read left-to-right, and the value landed underneath on the
     other side. The flex row already puts the value on the right. */
  .table-cards td.right { text-align: left; }

  /* The first column is the card's title, not another label/value pair. */
  .table-cards td.card-title {
    display: block;
    font-size: 16px;
    font-weight: 650;
    padding: 0 0 8px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--line);
  }
  .table-cards td.card-title::before { content: none; }

  /* Reordering is a mouse gesture — the handle arms `draggable` on mousedown,
     which no touch screen sends — so the grip is hidden rather than left there
     as a control that does nothing. The order is still set on a desktop and
     still pushes to the tills. */
  .table-cards .drag-cell { display: none; }

  /* The buttons get the whole width of the card and share it evenly, instead
     of being the thing hiding off the right-hand edge. */
  .table-cards td.row-actions-cell {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding: 12px 0 0;
    margin-top: 8px;
    border-top: 1px solid var(--line);
    text-align: left;
    white-space: normal;
  }
  .table-cards td.row-actions-cell::before { content: none; }
  .table-cards td.row-actions-cell .btn {
    flex: 1 1 96px;
    /* `td .btn + .btn` adds a left margin for the inline row; here the gap
       does that job and the margin only makes the last button short. */
    margin-left: 0;
  }
  .table-cards td.row-actions-cell .row-actions {
    flex: 1 1 100%;
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: 6px;
  }
  .table-cards td.row-actions-cell .row-actions .icon-btn {
    flex: 1 1 0;
    min-width: 0;
  }

  /* The sticky actions column is a rule about a table that scrolls sideways.
     Down here there is no table and no sideways: the cell is a block at the
     foot of a card, and left floating it would sit over the row above it. */
  .card > .table-cards tr > :last-child,
  .table-cards tr > :last-child {
    position: static;
    background: none;
    box-shadow: none;
  }

  /* "Nothing yet." is a colspan cell with no heading of its own. */
  .table-cards td[colspan] { display: block; padding: 4px 0; }
  .table-cards td[colspan]::before { content: none; }

  /* ---- Cells that are edited in place ------------------------------------
     The catalogue keeps its inline inputs and selects on a phone, and squeezed
     into whatever is left beside their label they are unreadable and unhittable
     — a select showing "Draught Beer" in 90px. So an editable cell stops being
     a label/value row and becomes a little form field: heading on its own line,
     control full width under it. */
  /* cellSelect() returns a <select class="cell-edit">, so the one class covers
     the text boxes and the dropdowns alike. */
  .table-cards td:has(> .cell-edit) { display: block; }
  .table-cards td:has(> .cell-edit)::before {
    display: block;
    margin-bottom: 5px;
  }
  .table-cards td .cell-edit {
    width: 100%;
    max-width: 100%;
    text-align: left;
  }

  /* The bulk-select box is under a heading that is itself a checkbox, so there
     is no word to take. It is the one column that gets told what it is here. */
  .table-cards td.pick-col {
    display: flex;
    gap: 10px;
    align-items: center;
    /* The one cell whose value is a control rather than a fact. Sent to the far
       side of a 690px card the box reads as belonging to nothing, and a
       checkbox is a flex item with no intrinsic stretch — so it was drawn in
       the middle of the space it had been given instead. It sits against its
       own word. */
    justify-content: flex-start;
  }
  .table-cards td.pick-col input { flex: none; }
  .table-cards td.pick-col::before {
    margin-right: 0;
    content: 'Select';
    color: var(--muted);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
  }

  /* The product picture sits above its name rather than beside a full-width
     input it would otherwise be pushed off the end of. */
  .table-cards td .thumb { margin: 0 0 8px; }

}

/* The foot of a list that loads as it is scrolled. Always present, so the
   space it needs is reserved rather than appearing under the last row and
   nudging the page as it arrives. */
.ex-more {
  min-height: 34px;
  padding: 12px 4px 4px;
  color: var(--muted);
  font-size: 12.5px;
  text-align: center;
}
.ex-more.loading::before {
  content: '';
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: 8px;
  vertical-align: -1px;
  border: 2px solid var(--line);
  border-top-color: var(--brand);
  border-radius: 50%;
  animation: ex-spin .7s linear infinite;
}
@keyframes ex-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .ex-more.loading::before { animation: none; }
}

/* ---- The report viewer ---------------------------------------------------
   Full screen with the chrome kept to a strip, because the thing worth looking
   at is the report. */
.pdfv {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, 2.5vh) max(12px, 2vw);
}
.pdfv-back {
  position: absolute;
  inset: 0;
  background: rgba(12, 12, 14, .62);
  backdrop-filter: blur(3px);
}
.pdfv-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(1080px, 100%);
  height: min(96vh, 100%);
  background: var(--card);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .45);
}

.pdfv-head {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 14px 18px;
  background: #111111;
  border-bottom: 3px solid var(--brand);
}
/* Shrinks rather than shoves: the viewer header carries three buttons that
   matter more than the mark does. */
.pdfv-logo { width: 100%; max-width: 135px; height: auto; flex: 0 1 auto; min-width: 60px; }
.pdfv-titles { flex: 1 1 200px; min-width: 0; }
.pdfv-titles h3 { color: #fff; font-size: 15px; }
.pdfv-sub { margin: 2px 0 0; font-size: 11.5px; color: #b8bcb0; }
.pdfv-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.pdfv-body {
  flex: 1 1 auto;
  min-height: 0;
  background: var(--surface-2);
  display: flex;
}
.pdfv-body iframe { flex: 1 1 auto; width: 100%; height: 100%; border: 0; }
.pdfv-state {
  margin: auto;
  padding: 24px;
  color: var(--muted);
  font-size: 13.5px;
  text-align: center;
}

@media (max-width: 720px) {
  .pdfv { padding: 0; }
  .pdfv-panel { height: 100%; border-radius: 0; }
  .pdfv-actions { width: 100%; }
  .pdfv-actions .btn { flex: 1 1 0; }
}


/* ---- Swipe cards ---------------------------------------------------------
   A worked example of each card, on the Cards page. The number is set in a
   monospaced face on purpose: this is a string somebody compares against a card
   in their hand, digit by digit, and a proportional font makes 1 and 7 and a
   run of zeroes harder to count than they need to be. */
.card-eg {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}
.card-eg:last-of-type { border-bottom: 0; }
.card-eg-label {
  min-width: 92px;
  font-weight: 600;
}
.card-eg-track {
  font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: .02em;
}
/* A programme the venue has switched off. Shown rather than hidden, because
   "not used" is an answer and a missing row is a question. */
.card-eg.off .card-eg-label { font-weight: 500; opacity: .65; }


/* ---- Wallet passes -------------------------------------------------------
   A preview of the card, not a mock-up of Apple's chrome. iOS draws the real
   thing and its rendering is not ours to promise; what this shows is the three
   things the venue actually controls -- the artwork, the colours and the words
   -- in the arrangement the pass puts them in. */
/* A pass, at the size a phone gives it.
   Apple lays out at 375pt wide; anything wider here is a preview that flatters
   a design which will not fit. Centred rather than stretched, so the panel can
   be any width without the card following it. */
.wal-pass {
  width: 100%;
  max-width: 320px;
  margin: 0 auto 16px;
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
  font-size: .9rem;
}
.wal-pass-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 13px 8px;
}
/* The chevron, at the size Apple actually draws it.
   22pt, against the 160pt wordmark that used to fill the whole logo slot and
   made every card -- whoever issued it -- read as an advertisement for Vesopa.
   Fixed height so a venue swapping in its own mark cannot resize the header. */
.wal-pass-mark {
  height: 22px;
  width: auto;
  flex: none;
  display: block;
}
.wal-pass-logo {
  font-weight: 700;
  letter-spacing: .01em;
  font-size: .82rem;
  /* The venue's name now, not the programme's. Truncates rather than wrapping
     the header onto two lines, which is what Wallet itself does with it. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The header field, top right. Right-aligned and pushed there by the margin
   rather than by justify-content, so the venue name keeps the space it needs
   and this takes what is left. */
.wal-pass-header {
  margin-left: auto;
  flex: none;
  text-align: right;
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-width: 45%;
}
.wal-pass-kind {
  font-size: .62rem;
  text-transform: uppercase;
  letter-spacing: .12em;
  font-weight: 700;
  white-space: nowrap;
}
/* The band of artwork, with the headline value sitting on it — which is where
   Apple puts the primary field, and why the points were missing from this
   preview before: there was nowhere for them to go.
   375x98, not 375x123. Every card is an eventTicket now — the only style
   besides boardingPass that honours groupingIdentifier, which is what collapses
   a venue's five cards into one stack — and an eventTicket strip is 98pt. The
   shipped artwork is rendered at exactly this ratio by tools/wallet_art, so a
   preview at the old height would be showing a crop the phone never makes. */
.wal-pass-strip {
  position: relative;
  aspect-ratio: 375 / 98;
  background-size: cover;
  background-position: center;
}
/* A scrim under the headline value.
   The strip is a photograph whose brightness is the venue's choice, and the
   label sits in the brand colour -- lime on a bright gold gradient is
   unreadable, and no amount of text-shadow fixes a mid-tone. Darkening the
   bottom third is what Apple does with its own strip layouts, and it is the
   only approach that works for artwork this code has never seen. */
.wal-pass-strip::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, .72) 0%, rgba(0, 0, 0, .35) 28%, transparent 58%);
  pointer-events: none;
}
.wal-pass-primary {
  z-index: 1;
  position: absolute;
  left: 13px;
  bottom: 10px;
  right: 13px;
  /* The artwork underneath is a photograph and its brightness is the venue's
     choice, so the text carries its own shadow rather than trusting contrast
     it cannot check. */
  text-shadow: 0 1px 4px rgba(0, 0, 0, .75);
}
.wal-pass-label {
  display: block;
  font-size: .58rem;
  text-transform: uppercase;
  letter-spacing: .14em;
  font-weight: 700;
  margin-bottom: 1px;
}
.wal-pass-value {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.05;
  display: block;
}
/* The secondary row. Wraps rather than scrolling: three fields is what fits on
   a phone, and a fourth belongs on the back of the card. */
.wal-pass-fields {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  padding: 11px 13px 4px;
}
.wal-pass-field { min-width: 0; }
.wal-pass-small {
  font-size: .8rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: block;
}
.wal-pass-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 10px 13px 13px;
  font-size: .68rem;
}
.wal-pass-venue {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Stands in for the barcode. Deliberately abstract: drawing a fake barcode
   would invite somebody to try scanning the preview. */
.wal-pass-code {
  flex: 1;
  height: 24px;
  border-radius: 4px;
  background: repeating-linear-gradient(
    90deg, currentColor 0 2px, transparent 2px 5px);
  opacity: .35;
}

.wal-qr { text-align: center; }
.wal-qr img {
  border-radius: 10px;
  background: #fff;
  padding: 8px;
}
.wal-qr p { word-break: break-all; margin-top: 8px; }

/* The QR arrives as inline SVG rather than an <img>, because /api/qr.svg is
   behind the session and an <img> cannot send the bearer token. Same white
   card the <img> rule above gives it. */
.wal-qr-img {
  display: inline-block;
  border-radius: 10px;
  background: #fff;
  padding: 8px;
  line-height: 0;
}
.wal-qr-img svg { display: block; width: 220px; height: 220px; }

/* The sign-up code, shown as the tail of a URL so it reads as one. */
.wal-slug {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--line, #263244);
  border-radius: 10px;
  overflow: hidden;
  margin: 6px 0;
}
.wal-slug-prefix {
  display: flex;
  align-items: center;
  padding: 0 8px;
  font-size: .82rem;
  color: var(--muted, #9aa7b4);
  background: rgba(255, 255, 255, .04);
  white-space: nowrap;
}
.wal-slug input {
  flex: 1;
  min-width: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.grid3 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 12px;
}
/* A colour input is a small swatch by default and impossible to aim at on a
   touch screen, which the back office is used on. */
.wal-colours input[type="color"] {
  width: 100%;
  height: 40px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: none;
}

.callout.warn {
  border-left: 3px solid #f77f00;
  background: rgba(247, 127, 0, .08);
  padding: 12px 14px;
  border-radius: 8px;
  margin: 10px 0;
}
.callout.warn ul { margin: 8px 0 0; padding-left: 1.1rem; }

/* The code, shown big enough to scan off the screen from across a desk. */
.wal-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  background: rgba(0, 0, 0, .6);
}
.wal-modal-card {
  background: var(--card, #fff);
  color: inherit;
  border-radius: 16px;
  padding: 24px;
  text-align: center;
  max-width: 22rem;
}
.wal-modal-card img {
  background: #fff;
  border-radius: 10px;
  padding: 10px;
  margin: 10px 0;
}

/* ---------------------------------------------------------------------------
   Row actions: a card on a phone, and a card on paper
   ---------------------------------------------------------------------------
   These two sit at the end of six different lists, so they are icons rather
   than words: "Wallet pass" and "Print card" in every row of a customer list
   pushes the name -- the thing being scanned for -- off the left of a tablet.
   Icon-only needs the tooltip and the aria-label that rowCardActions() gives
   them, which is why they are never drawn without one. */
.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--card);
  color: var(--muted);
  cursor: pointer;
  vertical-align: middle;
  transition: color .12s, border-color .12s, background .12s;
}
.icon-btn + .icon-btn,
.icon-btn + .btn,
.btn + .icon-btn { margin-left: 6px; }
.icon-btn svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon-btn:hover:not(:disabled) {
  color: var(--brand-deep);
  border-color: var(--brand);
  background: var(--brand-soft);
}
.icon-btn:disabled { opacity: .35; cursor: not-allowed; }

.wal-qr-box {
  background: #fff;
  border-radius: 10px;
  padding: 10px;
  margin: 10px 0;
  min-height: 260px;
  display: grid;
  place-items: center;
}
.wal-qr-box svg { display: block; width: 260px; height: 260px; }

/* The printed card, at the size a card actually is: 85.6 x 54mm, the same
   rectangle as a bank card, so what comes out of the printer can be held
   against a blank and cut to it. */
.print-card-wrap { display: grid; gap: 14px; justify-items: center; }
.print-card {
  width: 85.6mm;
  height: 54mm;
  box-sizing: border-box;
  padding: 5mm;
  border: 1px solid var(--line);
  border-radius: 3mm;
  background: #fff;
  color: #111;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.pc-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.pc-venue { font-weight: 700; font-size: 11pt; }
.pc-prog { font-size: 8pt; color: #555; }
.pc-body { display: flex; align-items: center; gap: 5mm; }
.pc-qr svg { display: block; width: 26mm; height: 26mm; }
.pc-who { display: flex; flex-direction: column; gap: 2mm; min-width: 0; }
.pc-name { font-size: 12pt; font-weight: 600; }
.pc-line { font-size: 8pt; color: #555; }
.pc-num { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10pt; letter-spacing: .5px; }

/* A voucher or a deposit receipt: taller than a card, because it is read
   rather than carried, and the conditions are the point. */
.print-slip {
  width: 80mm;
  box-sizing: border-box;
  padding: 8mm 6mm;
  border: 1px solid var(--line);
  border-radius: 2mm;
  background: #fff;
  color: #111;
  text-align: center;
}
.print-slip h4 { margin: 0 0 4mm; font-size: 12pt; letter-spacing: .04em; text-transform: uppercase; }
.ps-qr svg { display: block; width: 34mm; height: 34mm; margin: 0 auto; }
.ps-code {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 13pt;
  letter-spacing: 1px;
  margin: 3mm 0 5mm;
}
.ps-lines {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.5mm 4mm;
  text-align: left;
  margin: 0;
  font-size: 9pt;
}
.ps-lines dt { color: #666; }
.ps-lines dd { margin: 0; font-weight: 600; }

/* ---------------------------------------------------------------------------
   The Wallet screen: five tabs, and a designer inside the first
   --------------------------------------------------------------------------- */
/* Shared with the gym, which wears the same tab strip under its own
   class. Not the same class: a test asserts that exactly one WALLET tab
   starts open, and a second page borrowing the name made the gym part of
   the wallet's identity. */
.wal-tabs,
.gym-tabs {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin: 18px 0 16px;
  border-bottom: 1px solid var(--line);
}
.wal-tab,
.gym-tab {
  border: 0;
  background: none;
  padding: 10px 14px;
  font: inherit;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.wal-tab:hover,
.gym-tab:hover { color: var(--text); }
.wal-tab.on,
.gym-tab.on { color: var(--brand-deep); border-bottom-color: var(--brand); }

/* Which card, what it is, and the card itself — left to right, because that is
   the order the question is asked in. */
.wal-design {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) 375px;
  gap: 18px;
  align-items: start;
}
@media (max-width: 1200px) {
  .wal-design { grid-template-columns: 200px minmax(0, 1fr); }
  .wal-design-preview { grid-column: 1 / -1; }
}
@media (max-width: 780px) {
  .wal-design { grid-template-columns: minmax(0, 1fr); }
  .wal-kinds { flex-direction: row; overflow-x: auto; }
}

.wal-kinds { display: flex; flex-direction: column; gap: 6px; }
.wal-kind {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
  text-align: left;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
}
.wal-kind:hover { border-color: var(--brand); }
.wal-kind.on { border-color: var(--brand); background: var(--brand-soft); }
.wal-kind-name { font-weight: 600; }
.wal-kind-meta { display: flex; gap: 4px; flex-wrap: wrap; }
.wal-kind-meta .pill { font-size: 10.5px; }

.wal-colours-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
}
.wal-colour { display: grid; gap: 4px; }
.wal-colour .check.small { font-size: 12px; }

.wal-art { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.wal-art-preview {
  width: 250px;
  height: 82px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--bg);
}
.wal-art-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wal-art-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* A <label> wrapping a hidden file input has to look and behave like the button
   it is standing in for, including the pointer. */
.wal-art-actions label.btn { cursor: pointer; }

.wal-design-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
.wal-design-editor h4 {
  margin: 18px 0 8px;
  font-size: 13px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.pill.amber { background: rgba(247, 127, 0, .14); color: #a35200; }
.print-card-actions { display: flex; gap: 10px; justify-content: center; }

/* The venue's two pictures, on the Programme tab. A logo is a small mark and
   is shown at its own shape rather than stretched into the band's frame. */
.wal-art-preview.logo { width: 160px; height: 50px; }
.wal-art-preview.logo img { object-fit: contain; }
.wal-art [data-artclear][hidden] { display: none !important; }

/* The designer's colour swatches. Without a height an <input type="color">
   collapses to a line, which is a control nobody recognises as one. */
.wal-colours-grid input[type="color"] {
  width: 100%;
  height: 40px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: none;
  cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Permissions: the two screens that are a grid of switches.

   A role in this back office has fifty of them and a till group eleven, which
   is more than the modal's 440px column can hold without becoming a scroll of
   single-file checkboxes nobody can scan. So the modal widens and the switches
   sit in columns that reflow — read down a category, not through one.
   --------------------------------------------------------------------------- */

.perm-modal {
  width: min(880px, 94vw);
}

.perm-category {
  margin-top: 18px;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px 16px;
  background: var(--surface-2);
}

/* The category name and its "toggle all" on one line, because they are one
   thought: this section, and all of it. */
.perm-category-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
}

.perm-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 8px 18px;
}

/* Top-aligned: a two-line item next to a one-line item otherwise centres its
   checkbox against the middle of the text and the column stops reading as a
   column. */
.perm-item {
  align-items: flex-start;
  color: var(--text);
  padding: 4px 0;
}

.perm-item span { display: block; }
.perm-label { font-size: 14px; }

/* The explanation under a till permission. Present for every one of them: a
   switch a manager has to guess at is a switch that gets left on. */
.perm-hint {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-top: 2px;
  line-height: 1.35;
}

/* A named group or role, in a list. */
.pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--brand-soft);
  color: var(--brand-deep);
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
}

/* The empty state on both screens: an explanation and the one button that
   fixes it, rather than a blank table and a hunt for "Add". */
.empty-cta {
  text-align: center;
  padding: 28px 16px;
}
.empty-cta p { margin-bottom: 14px; }

/* The signed-in user's role, in the rail. "Why can I not see Products?" should
   be answerable without ringing anybody. */
#role-badge {
  display: block;
  margin: 8px 22px 0;
  font-size: 12px;
  color: var(--rail-text);
}

/* ---- The table card designer -------------------------------------------- */
/*
 * A canvas you drag on, replacing four number boxes per element. The stored
 * document is still percentages — one design has to print on an A4 sheet and a
 * 60mm sticker, and millimetres cannot do both — so the canvas works in pixels
 * for the duration of a drag and converts on drop.
 */
.dc-wrap { display: flex; flex-direction: column; gap: 12px; }
.dc-tools { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
.dc-toolgroup { display: flex; flex-wrap: wrap; gap: 6px; }
.dc-tools .btn { padding: 6px 10px; font-size: 13px; }

.dc-body { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
.dc-stage { flex: 0 0 auto; }

/* The card itself. `position: relative` is what every element is placed
   against, and the shadow is what makes it read as a piece of paper. */
/* THE CANVAS IS PAPER, NOT A PANEL.
 *
 * It inherited `color` from the application, so in Night mode every element
 * that had not been given a colour of its own was drawn in near-white — on a
 * white card. The card looked blank and nothing was wrong with the data.
 *
 * It does not follow the theme, and it should not: this is a preview of
 * something that is about to come out of a printer, and a preview that changes
 * with the room the manager is sitting in is not a preview. The paper colour is
 * the design's own — the venue can set it to anything, including black — and
 * the ink defaults to something that survives being printed.
 *
 * The chrome around it follows the theme as usual. */
.dc-page {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16);
  touch-action: none;          /* or a drag scrolls the page on a tablet */
  user-select: none;
  cursor: default;
  /* Explicit, so nothing inside can inherit a colour from the page around it. */
  color: #14161A;
  color-scheme: light;
}

/* In Night mode a white card on a near-black panel needs an edge and a lighter
   shadow, or it reads as a hole cut in the page. The card itself does not
   change — it is paper — but what it sits on does. */
:root[data-theme="dark"] .dc-page,
:root[data-theme="dark"] .dc-stage .dc-page {
  box-shadow: 0 12px 34px rgba(0, 0, 0, .55);
  border-color: rgba(255, 255, 255, .22);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dc-page {
    box-shadow: 0 12px 34px rgba(0, 0, 0, .55);
    border-color: rgba(255, 255, 255, .22);
  }
}
.dc-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* What it is, said next to it, so nobody wonders why it stayed white when they
   turned the lights off. */
.dc-paper-note {
  margin: 8px 0 0;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  max-width: 100%;
}

.dc-el { position: absolute; cursor: move; }
.dc-el img { width: 100%; height: 100%; object-fit: contain; display: block; }
.dc-el .dc-qr { width: 100%; height: 100%; display: grid; place-items: center; }
.dc-el .dc-qr svg { width: 100%; height: 100%; }
.dc-el .dc-text {
  width: 100%; height: 100%;
  display: flex; align-items: center;
  line-height: 1.15; overflow: hidden;
}
.dc-el .dc-placeholder {
  width: 100%; height: 100%;
  display: grid; place-items: center;
  background: repeating-linear-gradient(45deg,
    rgba(127,127,127,.10) 0 8px, rgba(127,127,127,.03) 8px 16px);
  font-size: 12px; color: var(--muted);
}

/* Only the selected element shows its edges. An outline on everything turns a
   finished card into a wireframe, and the point of this view is to look like
   what comes out of the printer. */
.dc-el.is-selected { outline: 2px solid var(--brand); outline-offset: 1px; }
.dc-handle {
  position: absolute; right: -6px; bottom: -6px;
  width: 14px; height: 14px; border-radius: 3px;
  background: var(--brand); border: 2px solid #fff;
  cursor: nwse-resize;
  box-shadow: 0 1px 4px rgba(0,0,0,.3);
}

.dc-panel {
  flex: 1 1 300px; min-width: 280px; max-width: 380px;
  border: 1px solid var(--line); border-radius: 10px; padding: 14px;
  background: var(--panel, var(--card-bg, transparent));
}
.dc-panel h4 { margin: 0 0 8px; font-size: 13px; letter-spacing: .04em;
               text-transform: uppercase; color: var(--muted); }
.dc-panel label { display: block; margin-bottom: 10px; }
.dc-panel .row { display: flex; }

.dc-themes { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }

.dc-layers { list-style: none; margin: 0; padding: 0; max-height: 260px;
             overflow: auto; }
.dc-layers li { margin: 0 0 4px; }
.dc-layers button {
  width: 100%; text-align: left; display: flex; align-items: center; gap: 8px;
  padding: 7px 9px; border: 1px solid var(--line); border-radius: 8px;
  background: none; color: inherit; font: inherit; cursor: pointer;
}
.dc-layers button:hover { border-color: var(--brand); }
.dc-layer-icon {
  width: 20px; height: 20px; flex: 0 0 auto; border-radius: 4px;
  display: grid; place-items: center; font-size: 12px;
  background: var(--brand-soft); color: var(--brand-deep);
}

.dc-library { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.dc-library button {
  width: 64px; height: 64px; padding: 4px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px; background: none;
}
.dc-library img { width: 100%; height: 100%; object-fit: contain; }


/* ===========================================================================
   DINE-IN, IN THE BACK OFFICE
   ===========================================================================

   `.card` carries a background, a border and a shadow and no padding at all —
   the padding lives in `.panel`, and everywhere else in this application the
   two are written together as `class="card panel"`. The four dine-in views
   were written with `class="card"` alone, so every one of them drew its
   heading, its hint and its inputs hard against the border on all four sides,
   with no gap at all between one card and the next. On a desktop that read as
   cramped. On an iPad it read as broken, which is where it was reported.

   Fixed here rather than by editing seven template strings, because the same
   fault would come back the next time a card is added, and because the
   responsive rules below want a scope to hang off anyway.
   ---------------------------------------------------------------------------
*/
/*
   The two-column layout broke this once already.
   These were direct-child selectors, and the settings page later grew a
   `.di-layout > .di-secs` wrapper around its panels — at which point every
   section on /dine-in silently lost its padding again and sat hard against its
   own border, which is exactly the fault the rule was written for. Measured:
   padding 0px on every section where a card elsewhere has 20.
   The wrapper is named here so the panels keep their padding wherever they are
   nested, and the preview beside them gets it too.
*/
#dinein-body > .card,
#dinein-body .di-secs > .card,
#dinein-body .di-side > .card,
#dinein_menu-body > .card,
#dinein_menu-body .di-secs > .card,
#dinein_qr-body > .card,
#dinein_orders-body > .card,
#dinein_menu-body .di-card {
  padding: var(--card-pad);
  margin-bottom: var(--stack, 20px);
}

/* These cards hold a heading and a hint above their table, so the card keeps
   its padding and the table simply fills what is left. Zeroing the card to let
   the table reach the border would put the heading on the border instead,
   which is the fault being fixed.

   The first and last cells lose their own side padding so the columns line up
   with the heading above them rather than sitting 12px further in. */
#dinein_qr-body > .card > table,
#dinein_orders-body > .card > table { width: 100%; }
#dinein_qr-body > .card > table th:first-child,
#dinein_qr-body > .card > table td:first-child,
#dinein_orders-body > .card > table th:first-child,
#dinein_orders-body > .card > table td:first-child { padding-left: 0; }
#dinein_qr-body > .card > table th:last-child,
#dinein_qr-body > .card > table td:last-child,
#dinein_orders-body > .card > table th:last-child,
#dinein_orders-body > .card > table td:last-child { padding-right: 0; }

/* A heading immediately followed by a hint had 14px above the hint and nothing
   above the heading, so the heading sat on the border. */
#dinein-body .card > h3:first-child,
#dinein_menu-body .card > h3:first-child,
#dinein_qr-body .card > h3:first-child,
#dinein_orders-body .card > h3:first-child { margin-top: 0; }

/* The web-address row: a fixed 260px field and a Check button beside a prefix
   that can be forty characters long. Below a certain width the three cannot
   share a line, and `flex:1` on every child was squeezing the button to the
   width of its own text minus the padding. */
#dinein-body .row { flex-wrap: wrap; row-gap: 10px; }
#dinein-body .row > * { flex: 0 1 auto; }
#dinein-body #di-slug { flex: 1 1 240px; min-width: 0; }

/* A printed table code is 32 characters with no spaces in it, so it cannot
   wrap at a word boundary and it was pushing the Ordering and Print columns
   off the right of the page. */
#dinein_qr-body td a { word-break: break-all; }

/* ---------------------------------------------------------------------------
   Tablet and below
   ---------------------------------------------------------------------------
   An iPad in portrait is 1024 CSS pixels — wider than most phones and narrower
   than the desk this was designed on, which is exactly the width that gets
   skipped. */
@media (max-width: 1180px) {
  /* The card designer is a canvas beside a panel of controls. Side by side it
     leaves the canvas about 300px, which is too small to drag anything in. */
  .dc-body { flex-direction: column; }
  .dc-stage { flex: 1 1 auto; width: 100%; }
  .dc-panel { width: 100%; max-width: none; }
  .dc-layers { max-height: 180px; }
}

@media (max-width: 820px) {
  /* Below this a table of tables is a worse way to read a list than a list is.
     Each row becomes a block with its column names in front of its values. */
  #dinein_qr-body > .card > table,
  #dinein_qr-body > .card > table tbody,
  #dinein_qr-body > .card > table tr,
  #dinein_qr-body > .card > table td { display: block; width: auto; }
  #dinein_qr-body > .card > table thead { display: none; }
  #dinein_qr-body > .card > table tr {
    padding: 14px 0;
    border-bottom: 1px solid var(--line);
  }
  #dinein_qr-body > .card > table tr:last-child { border-bottom: 0; }
  #dinein_qr-body > .card > table td {
    padding: 3px 0;
    border: 0;
    display: flex;
    gap: 10px;
    align-items: baseline;
    justify-content: space-between;
  }
  #dinein_qr-body > .card > table td::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: 12px;
    letter-spacing: .04em;
    text-transform: uppercase;
    flex: 0 0 auto;
  }
  #dinein_qr-body > .card > table td > * { min-width: 0; text-align: right; }
}

/* Fingers, not mice. A 22px checkbox is a miss more often than a hit, and the
   Ordering column on Table codes is a column of them. */
@media (pointer: coarse) {
  #dinein-body input[type="checkbox"],
  #dinein_qr-body input[type="checkbox"],
  #dinein_menu-body input[type="checkbox"] {
    width: 26px;
    height: 26px;
  }
  #dinein-body .btn,
  #dinein_menu-body .btn,
  #dinein_qr-body .btn,
  #dinein_orders-body .btn { min-height: 44px; }
}


/* ===========================================================================
   IN-PAGE MESSAGES AND QUESTIONS
   ===========================================================================
   Replacing window.alert and window.confirm, which iOS lets a person switch
   off permanently — see the note above `toast` in app.js. These animate in and
   out, because something that appears instantly next to what you were reading
   is harder to notice than something that arrives.
   ---------------------------------------------------------------------------
*/
#toast-host {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 900;
  display: flex;
  flex-direction: column-reverse;   /* newest nearest the thumb */
  gap: 10px;
  pointer-events: none;
  max-width: min(380px, calc(100vw - 36px));
}
.toast {
  pointer-events: auto;
  cursor: pointer;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-left: 4px solid var(--brand);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, .18);
  padding: 12px 15px;
  font-size: 14px;
  line-height: 1.4;
  opacity: 0;
  transform: translateY(10px) scale(.98);
  transition: opacity .18s ease, transform .18s cubic-bezier(.2, .8, .3, 1);
}
.toast.in { opacity: 1; transform: none; }
.toast-warn { border-left-color: var(--amber); }
.toast-error { border-left-color: var(--red); }

.confirm-back {
  opacity: 0;
  transition: opacity .18s ease;
}
.confirm-back.in { opacity: 1; }
.confirm-back .confirm-modal {
  max-width: 420px;
  transform: translateY(12px) scale(.97);
  transition: transform .22s cubic-bezier(.2, .8, .3, 1);
}
.confirm-back.in .confirm-modal { transform: none; }
.confirm-body { margin: 0 0 18px; color: var(--muted); line-height: 1.5; }

@media (prefers-reduced-motion: reduce) {
  .toast, .confirm-back, .confirm-back .confirm-modal { transition: none; }
}

/* Opening hours: seven rows that have to stay readable on a phone held in one
   hand at the end of a shift, which is when a landlord actually changes them. */
/* ===========================================================================
   THE FLOOR DESIGNER
   ===========================================================================
   A room is drawn and furnished here, not described. The strip along the top is
   the furniture; the plan below is the room; and while the room is being drawn
   the plan takes clicks as corners rather than as drags.
   --------------------------------------------------------------------------- */

.fd-tools { display: flex; flex-wrap: wrap; gap: 10px; }

/* ---- The furniture -------------------------------------------------------
   Scrolls sideways rather than wrapping: nine presets wrapped onto three rows
   push the plan — the thing the page is for — off the bottom of a laptop. */
.fd-palette {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding: 10px 12px;
  margin-top: 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  scrollbar-width: thin;
  scrollbar-color: var(--brand) transparent;
}
.fd-palette::-webkit-scrollbar { height: 8px; }
.fd-palette::-webkit-scrollbar-thumb {
  background: var(--brand);
  border-radius: 999px;
}

.preset {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  width: 74px;
  padding: 8px 6px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: grab;
}
.preset:hover { border-color: var(--brand); }
.preset:active { cursor: grabbing; transform: scale(.97); }

/* A drawing of the table, in its own proportions, so a six reads as long and a
   round four reads as round before the word underneath is read at all. */
.preset-art {
  display: block;
  width: calc(var(--pw) * 9px);
  height: calc(var(--ph) * 9px);
  border: 2px solid var(--brand);
  border-radius: 3px;
  background: color-mix(in srgb, var(--brand) 16%, transparent);
}
.preset-art.circle { border-radius: 50%; }
.preset-name { white-space: nowrap; }

/* ---- While the room is being drawn --------------------------------------- */
.fd-drawbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
  padding: 10px 14px;
  border: 1px solid var(--brand);
  border-radius: 10px;
  background: var(--brand-soft, rgba(165, 199, 21, .14));
  font-size: 13.5px;
  font-weight: 600;
}
.fd-drawbar > span { flex: 1 1 260px; }

/* The plan says what it is for. A crosshair rather than an arrow, because the
   click lands on a point and not on a thing. */
#canvas.drawing { cursor: crosshair; }
#canvas.drawing .tbl { pointer-events: none; opacity: .5; }

.lasso { pointer-events: none; }

/* A corner. Numbered, so it is obvious how many there are and which order the
   walls were walked in — and big enough to be hit by a finger rather than by a
   mouse, since this is drawn on a tablet standing in the room being drawn. */
.lasso .lasso-pt { pointer-events: auto; cursor: grab; touch-action: none; }
.lasso .lasso-pt circle {
  fill: var(--panel);
  stroke: var(--brand);
  stroke-width: 3;
}
.lasso .lasso-pt text {
  fill: var(--text);
  font-size: 10px;
  font-weight: 700;
  pointer-events: none;
  user-select: none;
}
.lasso .lasso-pt:hover circle { fill: color-mix(in srgb, var(--brand) 35%, var(--panel)); }
.lasso .lasso-pt:active { cursor: grabbing; }

/* The one that closes the room. Filled and pulsing, because "tap the first
   corner again" is only an instruction if the first corner is findable — on a
   plan that has been scrolled twice it otherwise means "remember where you
   started". */
.lasso .lasso-pt.first circle {
  fill: var(--brand);
  stroke: var(--panel);
  animation: lasso-pulse 1.6s ease-in-out infinite;
}
.lasso .lasso-pt.first text { fill: #10130A; font-size: 14px; }
@keyframes lasso-pulse {
  0%, 100% { stroke-width: 3; }
  50%      { stroke-width: 6; }
}
@media (prefers-reduced-motion: reduce) {
  .lasso .lasso-pt.first circle { animation: none; }
}

/* ---- A table that has been given a colour -------------------------------- */
.tbl { --tbl-colour: var(--brand); }
.tbl[style*="--tbl-colour"] {
  border-color: var(--tbl-colour);
  background: color-mix(in srgb, var(--tbl-colour) 20%, var(--panel));
}

.i-colour { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.i-colour input[type="color"] {
  width: 46px;
  height: 34px;
  min-height: 0;
  padding: 3px;
  margin: 0;
  border-radius: 8px;
  flex: 0 0 auto;
}

.i-code {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
.i-code p { margin: 4px 0 8px; }

@media (max-width: 720px) {
  .preset { width: 66px; }
}

/* ===========================================================================
   THE MENU-PAGE EDITOR
   ===========================================================================
   Nine panels and a preview. The panels fold, because a venue that set its
   colours in March should not scroll past them in June to reach its opening
   hours; the preview stays beside them, because every one of those panels
   changes what it shows and a preview you have to scroll to find is a preview
   nobody looks at.
   --------------------------------------------------------------------------- */

.di-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 18px;
  align-items: start;
}

.di-secs { display: flex; flex-direction: column; gap: var(--stack, 16px); min-width: 0; }

/* The preview travels with the scroll rather than sitting at the top of a page
   that is four screens long. Its own height is capped so it can never be taller
   than the window — a sticky element taller than its viewport stops sticking
   and simply scrolls away, which looks like the feature breaking. */
.di-side { position: sticky; top: 16px; min-width: 0; }
/* The preview is a folding section like every other panel on this page, so it
   brings its own card styling with it and needs no wrapper of its own. */
.di-preview .di-phone { margin-top: 4px; }
.di-side-note { margin: 8px 0 0; }

/* The preview, where it folds. It keeps the section styling the rest of the
   page uses, so the disclosure arrow and the heading read the same. */
.di-preview > summary h3 { margin: 0; }
.di-preview[open] > summary { margin-bottom: 12px; }

/* A phone, drawn around the preview so what is inside it reads as a phone
   screen rather than as part of the form. The aspect ratio is a real handset's
   and the iframe is scaled into it, so the preview shows the layout a customer
   gets rather than a squeezed desktop one. */
.di-phone {
  position: relative;
  width: 100%;
  aspect-ratio: 390 / 760;
  border: 8px solid var(--rail, #17141c);
  border-radius: 26px;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 10px 30px -18px rgba(0, 0, 0, .6);
}
.di-phone iframe {
  position: absolute;
  inset: 0;
  width: 390px;
  height: 760px;
  border: 0;
  /* Rendered at a phone's own width and then scaled down, so the page inside
     lays itself out the way it would on a phone. An iframe simply given the
     panel's width would be a 320px desktop, which is a layout no customer
     ever sees. */
  transform-origin: 0 0;
  transform: scale(var(--di-phone-scale, .78));
}

@media (max-width: 1100px) {
  /* No room for a phone beside a form.
     The preview stays at the top — it is the first thing a venue wants to see
     when it opens this page — but folded, because open it is a 708px block and
     every setting on the page starts below it. app.js opens it again as soon
     as there is room for two columns. */
  .di-layout { grid-template-columns: minmax(0, 1fr); }
  .di-side { position: static; order: -1; }
  .di-phone { max-width: 320px; margin: 0 auto; }
}

/* ---- A panel that folds --------------------------------------------------
   The heading is the control, so there is nothing extra to aim at, and the
   marker is drawn rather than left to the browser: the default triangle is a
   different shape, size and colour in every browser this is used in. */
.di-sec > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  list-style: none;
  margin: -2px 0 0;
  padding-right: 26px;
  position: relative;
}
.di-sec > summary::-webkit-details-marker { display: none; }
.di-sec > summary h3 { margin: 0; }
.di-sec > summary::after {
  content: "";
  position: absolute;
  right: 4px;
  top: 50%;
  width: 9px;
  height: 9px;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: translateY(-70%) rotate(45deg);
  transition: transform .18s ease;
}
.di-sec[open] > summary::after { transform: translateY(-20%) rotate(-135deg); }
.di-sec > summary:hover::after { border-color: var(--text); }
.di-sec[open] > summary { margin-bottom: 12px; }

/* ---- A colour, shown as a colour ----------------------------------------
   The native control is a small grey rectangle with a hairline of colour in
   it. Here the swatch is the control: a round well of the colour itself, with
   the real input lying invisibly over it so a tap still opens the operating
   system's own picker and keyboard users still reach it by tab. */
.di-swatch { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.di-swatch-well {
  position: relative;
  width: 38px;
  height: 38px;
  flex: 0 0 auto;
  border-radius: 999px;
  border: 1px solid var(--line);
  box-shadow: inset 0 0 0 2px var(--panel);
  overflow: hidden;
}
.di-swatch-well input[type="color"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
}
.di-swatch-well:focus-within {
  box-shadow: inset 0 0 0 2px var(--panel), 0 0 0 3px var(--brand-soft);
}
.di-swatch-hex {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 12.5px;
  color: var(--muted);
  letter-spacing: .04em;
}

/* ---- A section of the menu ----------------------------------------------- */
.di-menu-sec > summary { justify-content: flex-start; }
.di-menu-sec > summary h3 { flex: 0 1 auto; min-width: 0; }
/* Clear of the fold arrow, which is drawn at the summary's right edge. At
   10px the count sat under it and the two read as one smudged glyph. */
.di-sec-count { margin-left: auto; padding-right: 22px; white-space: nowrap; }

/* The name, the line underneath, and the two buttons.
   The buttons were flex items in a row of flexible fields and were being
   stretched with them — a save icon two hundred pixels wide beside a delete
   icon the same, which is what "too much width, not friendly" was. They are
   icons: they are as wide as an icon. */
.di-sec-fields {
  display: grid;
  /* `auto` sized itself to the narrowest the buttons could be squeezed to,
     which was one of them; the pair needs room to sit in a row. */
  grid-template-columns: minmax(0, 1fr) max-content;
  gap: 4px 10px;
  align-items: start;
}
/* The label supplies its own top margin, so the grid gap only has to separate
   the second field from the first. */
.di-sec-fields > label { margin: 0; }
.di-sec-fields > label:first-child { margin-top: 0; }
.di-sec-fields > .float:first-child { grid-column: 1; }
.di-sec-fields > .float:nth-child(2) { grid-column: 1; }
/* Save and delete, side by side and level with the field they belong to.
   Stacked in a 40px column they took two lines to say what fits on one, and
   the pair straddled the two fields rather than sitting against either. */
.di-sec-acts {
  grid-column: 2;
  grid-row: 1 / span 2;
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 8px;
  flex: 0 0 auto;
  /* Level with the first input rather than with the label above it. */
  padding-top: 22px;
}
.di-sec-acts .ibtn, .di-sec-acts .icon-btn { flex: 0 0 auto; }

/* The dishes. Six columns do not fit a tablet, so the table scrolls inside its
   own box rather than pushing the page sideways — which would take the name,
   the one column that says which dish the row is, off the left edge. */
/* The product picker scrolls its list and nothing else.
   It had two bars: the list's own, and the panel around it scrolling by the
   seventeen pixels its heading and footer overran. Two scrollbars side by side
   is a dialog that looks broken, and the outer one moves almost nothing. The
   panel is a column that fits the window; the list is the part that flexes. */
/* The panel around it stops scrolling; the list inside it does the scrolling.
   Without this both scroll, which is the two bars that were reported. */
.panel-body:has(> .di-pick-panel) {
  max-height: none;
  overflow: visible;
}
.di-pick-panel {
  display: flex;
  flex-direction: column;
  max-height: min(78vh, 640px);
  overflow: hidden;
}
.di-pick-panel > .di-pick-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.di-scroll {
  margin-top: 12px;
  overflow-x: auto;
  /* Keep the sideways gesture in this box rather than handing it to the page
     once the end is reached. Without it iOS passes the overscroll outward and
     the swipe that should bring the first column back instead rubber-bands the
     whole view, which is what "the x axis scroll is blocked" felt like. */
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}
/* Fixed, so the percentages on the headings are what the columns get. Left to
   the browser, the three tick columns and the three action buttons took the
   width they wanted and the two columns that actually hold writing were
   squeezed to about 170px each — every dish on the menu reading "Garlic Bre".*/
/* Narrow enough to fit a tablet with the rail open.
   Measured there: the scroll box is 734px and the table was 860, so it scrolled
   — and a horizontal scroller full of text inputs is a trap, because tapping
   one makes the browser scroll it into view and a finger then has to find its
   way back to a first column that is off the left edge. That is what "cannot
   scroll to the left" was. Fitting removes the gesture rather than fixing it;
   the overflow rule stays for genuinely small screens. */
.di-items { min-width: 700px; table-layout: fixed; }
.di-items td { overflow: hidden; }
.di-items th.mid, .di-items td.mid { text-align: center; }

/* Tight cells, wrapping headings.
   These columns are deliberately narrow — they hold a tick — and the padding a
   normal table cell carries was taking more of them than the tick did, so
   "Popular" and "Featured" were clipped and the header row read as one word.
   Letting the heading wrap costs a few pixels of height once, at the top of
   the table, and nothing else. */
.di-items th, .di-items td { padding-left: 6px; padding-right: 6px; }
.di-items th { white-space: normal; line-height: 1.15; vertical-align: bottom; }

/* Room for three buttons and the gaps between them.
   Measured: the third — delete — was drawn outside its cell and clipped by the
   scroll box, which could not scroll because the table itself fitted. It was on
   the page and unreachable by any means. */
.di-items td.di-row-acts { padding-left: 2px; padding-right: 2px; }
.di-items td.di-row-acts {
  text-align: right;
  white-space: nowrap;
}
.di-items input[type="text"], .di-items input:not([type]) { margin-top: 0; }

@media (max-width: 720px) {
  /* Room for the fields rather than for the buttons, which go under them. */
  .di-sec-fields { grid-template-columns: minmax(0, 1fr); }
  .di-sec-acts { grid-column: 1; grid-row: auto; flex-direction: row; }
}

/* ---- Choosing a picture --------------------------------------------------
   The file input is invisible and the label is the button, because a browser's
   own file input cannot be styled and says "No file chosen" in the middle of a
   form that has nothing else in that voice. */
.img-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.filepick-btn { position: relative; overflow: hidden; cursor: pointer; }
.filepick-btn input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}
.filepick-btn:focus-within {
  outline: none;
  box-shadow: 0 0 0 3px var(--brand-soft);
}

.di-hours { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
.di-hour-row {
  display: grid;
  grid-template-columns: 108px 104px 1fr auto 1fr;
  align-items: center;
  gap: 10px;
}
.di-hour-row .di-day { font-weight: 600; }
.di-hour-row input[type="time"] { width: 100%; min-width: 0; }
.di-hour-row .di-shut { margin: 0; }
.di-hour-row input:disabled { opacity: .45; }

@media (max-width: 700px) {
  /* The day and the switch on one line, the two times on the next. Squeezing
     five controls onto a 360px line gives time fields too narrow to show the
     minutes, which is the one thing they exist to show. */
  .di-hour-row {
    grid-template-columns: 1fr auto;
    row-gap: 6px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--line);
  }
  .di-hour-row .di-day { grid-column: 1; }
  .di-hour-row .di-shut { grid-column: 2; justify-self: end; }
  .di-hour-row input[type="time"] { grid-column: 1; }
  .di-hour-row .di-to { display: none; }
  .di-hour-row input[data-hclose] { grid-column: 2; }
}


/* ===========================================================================
   THE SHELL: a rail that folds, a theme corner, and lockups per theme
   =========================================================================== */

/* ---------------------------------------------------------------------------
   One lockup per theme
   ---------------------------------------------------------------------------
   The wordmark exists in two colourways and only one of them is legible in a
   given room. Both are in the document and CSS chooses, rather than JavaScript
   swapping a src — because under "Auto" the system can change while the page
   is open, and a media query notices that on its own.
   --------------------------------------------------------------------------- */
/* Both carry the same cap the sizing classes do. The lockup is 900x130 — nearly
   seven to one — so anything that can hold it must refuse to let it exceed its
   column; `backoffice-layout.test.js` checks every class the mark is drawn
   with, and it is right to, because these sit alongside .brand-logo and
   .topbar-logo and would otherwise be the one way round the rule. */
.lockup-dark { display: none; max-width: 100%; height: auto; }
.lockup-light { display: block; max-width: 100%; height: auto; }
:root[data-theme="dark"] .lockup-dark { display: block; }
:root[data-theme="dark"] .lockup-light { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lockup-dark { display: block; }
  :root:not([data-theme="light"]) .lockup-light { display: none; }
}

/* The rail keeps a visible edge against the page in Day mode; in Night the two
   are different enough not to need one. */
.rail { border-right: 1px solid var(--rail-line); }

/* ---------------------------------------------------------------------------
   Folding the rail
   ---------------------------------------------------------------------------
   Collapsed means gone, not narrow.

   The first version of this kept a 68px strip of icons, on the reasoning that
   every view stayed one press away. That was the wrong trade: the point of
   collapsing is to give the page its width back, and a strip of unlabelled
   glyphs charges 68px for a menu you still have to decode. Hidden entirely, the
   page gets all of it, and one press on the corner brings the whole rail back
   with its words on.

   It slides rather than blinks. A panel that appears instantly beside what you
   were reading is easy to miss; one that arrives from the edge tells you where
   it came from and where it will go.
   --------------------------------------------------------------------------- */
.rail-fold {
  display: none;                 /* desktop only; see the media query below */
  align-items: center;
  gap: 9px;
  margin: 10px 12px 0;
  padding: 9px 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--rail-group);
  font-size: 13px;
  cursor: pointer;
}
.rail-fold:hover { background: var(--rail-hover); color: var(--rail-active); }
.rail-fold-icon {
  width: 18px; height: 18px; flex: 0 0 auto;
  stroke: currentColor; fill: none; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

/* The button that brings it back. Fixed in the opposite corner from the theme
   control and built the same way, so the two corners of the chrome match. */
.rail-show {
  position: fixed;
  top: 14px;
  left: 16px;
  z-index: 70;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--text);
  cursor: pointer;
  display: none;
  place-items: center;
  box-shadow: var(--shadow);
  opacity: 0;
  transform: scale(.8);
  transition: opacity .18s ease .12s, transform .18s cubic-bezier(.2,.9,.3,1) .12s,
              border-color .15s ease;
}
.rail-show:hover { border-color: var(--brand); }
.rail-show svg {
  width: 19px; height: 19px;
  stroke: currentColor; fill: none; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

@media (min-width: 961px) {
  .rail-fold { display: flex; }

  /* The rail is a flex child of #app, so its width is what gives the page its
     room back. Animating width and transform together is what makes it look
     like it is leaving rather than being deleted. */
  .rail {
    transition: width .26s cubic-bezier(.4, 0, .2, 1),
                transform .26s cubic-bezier(.4, 0, .2, 1);
  }
  #app.rail-folded .rail {
    width: 0;
    transform: translateX(-100%);
    /* Nothing inside may be reached or read while it is away. */
    pointer-events: none;
    visibility: hidden;
    border-right: 0;
  }
  #app.rail-folded .rail-show { display: grid; opacity: 1; transform: none; }

  /* The theme control keeps its corner; the page simply has more of the middle. */
  /* The page keeps the same inset on both sides once the rail is gone.
   *
   * It was 68px on the left against 36px on the right, to clear the hamburger —
   * which put every page permanently off-centre for the sake of one button in
   * one corner. The button only overlaps the heading row, so the heading row is
   * what makes room for it, exactly as it already does on the right for the
   * theme control. */
  #app.rail-folded .page-head { padding-left: 56px; }
}

@media (prefers-reduced-motion: reduce) {
  .rail, .rail-show { transition: none; }
}

/* ---------------------------------------------------------------------------
   The theme corner
   --------------------------------------------------------------------------- */
.theme-corner { position: fixed; top: 14px; right: 16px; z-index: 70; }

.theme-btn {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--text);
  cursor: pointer;
  display: grid;
  place-items: center;
  box-shadow: var(--shadow);
  transition: transform .12s ease, border-color .15s ease;
}
.theme-btn:hover { border-color: var(--brand); }
.theme-btn:active { transform: scale(.94); }
.theme-btn svg {
  width: 19px; height: 19px;
  stroke: currentColor; fill: none; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
  grid-area: 1 / 1;
}

/* Which of the three shows is the choice, not the resolved theme: somebody on
   Auto wants to see that they are on Auto. */
.theme-btn .ic-sun, .theme-btn .ic-moon, .theme-btn .ic-auto { display: none; }
:root[data-theme="light"] .theme-btn .ic-sun { display: block; }
:root[data-theme="dark"] .theme-btn .ic-moon { display: block; }
:root:not([data-theme]) .theme-btn .ic-auto { display: block; }

.theme-menu {
  position: absolute;
  top: 48px;
  right: 0;
  min-width: 168px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 13px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, .18);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  transform-origin: top right;
  animation: theme-pop .16s cubic-bezier(.2, .9, .3, 1);
}
@keyframes theme-pop {
  from { opacity: 0; transform: scale(.94) translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.theme-menu button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  border: 0;
  border-radius: 9px;
  background: none;
  color: var(--text);
  padding: 10px 11px;
  font-size: 14px;
  cursor: pointer;
  text-align: left;
}
.theme-menu button:hover { background: var(--hover); }
.theme-menu button[aria-checked="true"] {
  background: var(--brand-soft);
  color: var(--brand-deep);
  font-weight: 600;
}
.theme-menu svg {
  width: 17px; height: 17px; flex: 0 0 auto;
  stroke: currentColor; fill: none; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

@media (max-width: 960px) {
  /* On the bar's own centre line, and inset the same as the control opposite.
     The top bar is 56px and the button is 40, so 8px of it above and 8 below
     puts its middle on 28 — where the hamburger's middle, the wordmark's and
     the live dot's already are. Ten pixels from the right against the
     hamburger's twelve from the left made a bar that was symmetrical
     everywhere except its two ends. */
  .theme-corner { top: 8px; right: 12px; }
  .theme-btn {
    background: transparent;
    border-color: var(--rail-line);
    color: var(--rail-text);
    box-shadow: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .theme-menu { animation: none; }
}

/* ===========================================================================
   FORM CONTROLS THAT LOOK THE SAME ON EVERY DEVICE
   ===========================================================================
   iOS draws its own select, date and time fields: its own height, its own
   font, its own inset chevron, its own centred text, and a blue tint that
   belongs to Safari rather than to this application. Beside a text input
   styled here they were visibly a different control from a different program.

   `-webkit-appearance: none` is what stops that, and everything below is what
   has to be supplied once the platform stops supplying it.
   --------------------------------------------------------------------------- */
select,
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-color: var(--input-bg);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 3px 13px;
  font-family: inherit;
  /* `font: inherit` was the whole of it, and it takes the weight of whatever
     the field sits inside — which in a filter bar is a 600-weight label, so the
     date read bolder than the text box beside it. The family is worth
     inheriting; the weight is not. */
  font-weight: 400;
  font-size: 15px;
  line-height: 1.2;
  width: 100%;
  box-sizing: border-box;
  min-height: 44px;         /* a finger, not a mouse */
}

/* Sixteen pixels, but only where it buys something.
   Safari zooms the page in when a field it is focusing is under 16px, and that
   zoom does not come back out — which is why this was 16 everywhere. But it is
   only touch Safari that does it, and at 16 these fields were a pixel larger
   than every other input on a desktop, which is exactly the mismatch being
   fixed here. */
@media (pointer: coarse) {
  select,
  input[type="date"],
  input[type="time"],
  input[type="datetime-local"],
  input[type="month"],
  input[type="week"] { font-size: 16px; }
}

/* The chevron the platform stopped drawing. An inline SVG in a data: URI, so
   there is no request for it and it cannot arrive late. */
select {
  padding-right: 38px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2378717f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 17px;
}
:root[data-theme="dark"] select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a2ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select {
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a2ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  }
}

/* Safari centres the value in a date field and leaves a gap where its own
   chrome used to be; these put it back on the left with everything else. */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
  text-align: left;
  -webkit-text-fill-color: var(--text);   /* an empty date is grey otherwise */
}
input[type="date"]::-webkit-date-and-time-value,
input[type="time"]::-webkit-date-and-time-value,
input[type="datetime-local"]::-webkit-date-and-time-value {
  text-align: left;
  margin: 0;
}
input[type="date"]::-webkit-datetime-edit,
input[type="time"]::-webkit-datetime-edit,
input[type="datetime-local"]::-webkit-datetime-edit { padding: 0; }

/* The picker button: Safari's is a blue glyph in its own accent colour, which
   is the one part of these fields that still announced the browser. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  opacity: .55;
  cursor: pointer;
  filter: none;
}
:root[data-theme="dark"] input::-webkit-calendar-picker-indicator {
  filter: invert(1);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) input::-webkit-calendar-picker-indicator {
    filter: invert(1);
  }
}

select:focus,
input[type="date"]:focus,
input[type="time"]:focus,
input[type="datetime-local"]:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}
select:disabled,
input[type="date"]:disabled,
input[type="time"]:disabled { opacity: .55; cursor: not-allowed; }

/* A select inside a row that sizes itself must not be forced to 100%. */
.row select, .seg select, .dc-panel select, .di-hour-row input[type="time"] {
  width: 100%;
}


/* The sign-in page's copy of the theme control. Same component, and it has to
   clear the card rather than sit on it on a narrow screen. */
#theme-corner-login { z-index: 80; }
@media (max-width: 640px) {
  #theme-corner-login { top: 10px; right: 10px; }
  #theme-corner-login .theme-btn { background: var(--card); border-color: var(--line); }
}


/* ===========================================================================
   ICON BUTTONS
   ===========================================================================
   A row of actions written out in words — Save, Remove, Duplicate — is three
   buttons' worth of text in a column that has to hold a dish name as well, and
   on a tablet it pushes the whole table sideways.

   HOW ANYBODY KNOWS WHAT THEY DO

   Three ways, because no single one covers everybody:

     * hovering, on a machine with a pointer;
     * pressing and holding, on a touchscreen, where :hover either never fires
       or sticks on after the finger has gone;
     * and a legend under the table, which needs no discovery at all and is the
       only one that works for somebody who has not yet touched anything.

   The name is on the button as `data-tip`, and the same string is the
   aria-label, so there is one place to write it and no way for the two to
   disagree.
   --------------------------------------------------------------------------- */
.ibtn,
.icon-btn {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--muted);
  cursor: pointer;
  display: inline-grid;
  place-items: center;
  padding: 0;
  vertical-align: middle;
  transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.ibtn + .ibtn,
.icon-btn + .icon-btn { margin-left: 6px; }
/* Material Symbols are filled shapes on a 0 -960 960 960 canvas, not stroked
   outlines. Left as `fill: none; stroke: currentColor` — which is what the
   hand-drawn marks needed — every button rendered as an empty box: present,
   pressable, and showing nothing at all. */
.ibtn svg,
.icon-btn svg {
  width: 19px; height: 19px;
  fill: currentColor; stroke: none;
  pointer-events: none;                 /* so the click target is the button */
}
.ibtn:hover,
.icon-btn:hover { color: var(--text); border-color: var(--brand); background: var(--hover); }
.ibtn:active,
.icon-btn:active { transform: scale(.93); }
.ibtn:focus-visible,
.icon-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.ibtn[disabled],
.icon-btn[disabled] { opacity: .4; cursor: not-allowed; }

/* Destructive actions are the one kind worth colouring, and only on approach —
   a row of red buttons reads as a row of errors. */
.ibtn.danger:hover,
.icon-btn.danger:hover { color: #fff; background: var(--red); border-color: var(--red); }
.ibtn.go:hover,
.icon-btn.go:hover { color: var(--on-brand); background: var(--brand); border-color: var(--brand); }

/* The name, on hover or on a long press. Drawn from the attribute, so there is
   no second element to keep in step and nothing to clean up. */
.ibtn::after,
.icon-btn::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%) translateY(3px);
  background: var(--text);
  color: var(--card);
  padding: 5px 9px;
  border-radius: 7px;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .13s ease, transform .13s ease;
  z-index: 60;
}
.ibtn:hover::after,,
.icon-btn:hover::after,
.ibtn:focus-visible::after,,
.icon-btn:focus-visible::after,
.ibtn.tip-open::after,
.icon-btn.tip-open::after { opacity: 1; transform: translateX(-50%); }

/* A tooltip on the last button in a row would otherwise be cut off by the
   edge of the card it sits in. */
.ibtn:last-child::after,
.icon-btn:last-child::after { left: auto; right: 0; transform: translateY(3px); }
.ibtn:last-child:hover::after,,
.icon-btn:last-child:hover::after,
.ibtn:last-child:focus-visible::after,,
.icon-btn:last-child:focus-visible::after,
.ibtn:last-child.tip-open::after,
.icon-btn:last-child.tip-open::after { transform: none; }

@media (pointer: coarse) {
  /* Nothing hovers on a touchscreen, and what :hover does there is stick on
     after the finger has gone. Long press opens it; see wireTips in app.js. */
  .ibtn:hover::after,
  .icon-btn:hover::after { opacity: 0; }
  .ibtn.tip-open::after,
  .icon-btn.tip-open::after { opacity: 1; }
  .ibtn,
  .icon-btn { width: 40px; height: 40px; }
}

/* The legend. Under the table it belongs to, so somebody who has not touched
   anything yet can still read what the column of buttons is for. */
.ibtn-key,
.icon-btn-key {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 12.5px;
  color: var(--muted);
}
.ibtn-key span,
.icon-btn-key span { display: inline-flex; align-items: center; gap: 6px; }
.ibtn-key svg,
.icon-btn-key svg {
  width: 16px; height: 16px;
  fill: currentColor; stroke: none;
}

@media (prefers-reduced-motion: reduce) {
  .ibtn, .ibtn::after,
  .icon-btn, .icon-btn::after { transition: none; }
}


/* ===========================================================================
   TAPS THAT LAND, AND A PAGE THAT SAYS IT IS WORKING
   =========================================================================== */

/* WHY THE RAIL FELT SLOW ON AN IPAD
 *
 * Safari holds a tap for about 300ms on any element it thinks might be
 * double-tapped to zoom, and it decides that from the layout rather than from
 * the element. A nav button in a fixed column qualifies. The press had already
 * happened and the browser was waiting to find out whether a second one was
 * coming — which reads exactly as "sometimes it does not respond".
 *
 * `touch-action: manipulation` says this element is never double-tapped, and
 * the delay goes. The tap highlight goes with it because a 300ms grey flash
 * arriving after the view has changed looks like a second, late press.
 */
.nav,
.nav-group,
.btn,
.ibtn,,
.icon-btn,
.theme-btn,
.rail-show,
.rail-fold,
.rail-toggle,
.seg-btn,
.room-tab,
button {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* A pressed state that is instant, so the first tap is acknowledged before any
   data has been asked for. Waiting for the view to load before showing that
   anything happened is what makes a fast page feel slow. */
.nav:active { background: var(--rail-hover); }
.nav.is-going { background: var(--brand); color: var(--on-brand); }

/* ---------------------------------------------------------------------------
   The progress bar
   ---------------------------------------------------------------------------
   A thin line across the top, the way a browser or YouTube does it. It creeps
   towards the end while the request is out and finishes when it lands, because
   a bar that reports a real percentage cannot: the server has not said how much
   there is.
   --------------------------------------------------------------------------- */
#loadbar {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 0;
  z-index: 100;
  background: linear-gradient(90deg, var(--brand), var(--brand-deep));
  box-shadow: 0 0 8px var(--brand);
  opacity: 0;
  transition: width .2s ease, opacity .25s ease;
  pointer-events: none;
}
#loadbar.on { opacity: 1; }

/* ---------------------------------------------------------------------------
   Skeletons
   ---------------------------------------------------------------------------
   The shape of what is coming, in the place it will come. A spinner in the
   middle of an empty page says "wait"; this says "cards, here, shortly" — and
   when the real thing lands nothing jumps, because it is already the right
   size.
   --------------------------------------------------------------------------- */
.sk { display: grid; gap: 14px; }
.sk-line, .sk-card, .sk-stat {
  border-radius: 10px;
  background: linear-gradient(90deg,
    var(--track) 0%, var(--hover) 40%, var(--track) 80%);
  background-size: 300% 100%;
  animation: sk-sweep 1.25s ease-in-out infinite;
}
.sk-line { height: 13px; }
.sk-line.w40 { width: 40%; }
.sk-line.w70 { width: 70%; }
.sk-card { height: 128px; border-radius: var(--radius); }
.sk-stat { height: 92px; border-radius: var(--radius); }
.sk-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 16px; }

@keyframes sk-sweep {
  from { background-position: 130% 0; }
  to   { background-position: -30% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .sk-line, .sk-card, .sk-stat { animation: none; }
  #loadbar { transition: opacity .25s ease; }
}


/* A view that is waiting for its data.
 *
 * The heading stays — it is already correct, and replacing it with a grey bar
 * would be a step backwards. Everything below it is hidden and the skeleton
 * stands in, by adding a class rather than by emptying the view: no loader has
 * to cooperate, nothing is destroyed, and when the class comes off whatever the
 * loader wrote is already underneath. */
.view.is-loading > *:not(.page-head):not(.view-skeleton) { display: none !important; }
.view-skeleton { margin-top: 4px; }


/* The corner control is fixed, so it sits over whatever the page puts in its
   top right — which on most pages is the primary button. The heading row keeps
   a lane clear for it rather than the control moving, because the control has
   to be in the same place on every page to be found at all. */
.page-head { padding-right: 56px; }
@media (max-width: 960px) {
  /* The control moves into the top bar there, so the lane is not needed. */
  .page-head { padding-right: 0; }
}


/* ===========================================================================
   ONE HEIGHT FOR EVERY CONTROL ON A ROW
   ===========================================================================
   A select is drawn by the platform and a button is drawn by us, so left alone
   they never match: on Financial Summary the PDF/CSV/XLS picker sat two pixels
   proud of Run report, and on Sales Explorer and Screen programming the same
   thing again beside every button next to it.

   The fix is one number in one place. --control-h is what a button, a select,
   a text input and a file picker are all set to, so a row of them is a row of
   one height whatever it is made of.
   --------------------------------------------------------------------------- */
:root { --control-h: 40px; }

.btn,
select,
input[type="text"], input[type="search"], input[type="email"],
input[type="password"], input[type="number"], input[type="tel"],
input[type="url"], input[type="date"], input[type="time"],
input[type="datetime-local"], input[type="month"], input[type="week"] {
  height: var(--control-h);
  min-height: var(--control-h);
  box-sizing: border-box;
  /* Vertically centred against each other rather than sitting on their own
     baselines, which is the other half of why they looked mismatched. */
  vertical-align: middle;
}

/* Anything that grows with its content keeps its own height. */
textarea, select[multiple], select[size] { height: auto; min-height: 0; }

/* The small variant is small for all of them, not just for buttons.
 *
 * 36 rather than 34: backoffice-tablet.test.js presses everything on a 1366px
 * tablet and refuses anything under 36px, which is the right refusal — 34px is
 * a thumb's width of miss. On a touchscreen they go to 40. */
.btn.small, .seg-btn, .row.compact select, .row.compact .btn {
  height: 36px;
  min-height: 36px;
}
@media (pointer: coarse) {
  .btn.small, .seg-btn, .row.compact select, .row.compact .btn {
    height: 40px;
    min-height: 40px;
  }
}

/* Five ranges across a phone, still as one joined control.
 *
 * It has to stay one line: a segmented picker IS the row of choices, and
 * wrapping it onto two rows -- or scrolling it, which hides two of the five --
 * turns a control you read at a glance into one you have to work out.
 *
 * So the row is made to fit rather than allowed to break. The segments share
 * the width equally, the padding comes in, and each label has a short form that
 * only a phone sees: "Last 30 days" is the right words on a desktop and
 * "30 days" is the same meaning in the space actually available. See the
 * .seg-long / .seg-short pair below.
 *
 * The kitchen's three-way control is excluded -- one-word labels, and its own
 * stacking treatment at 760px.
 */
@media (max-width: 560px) {
  .seg:not(.kds-mode-seg) {
    display: flex;
    width: 100%;
  }
  .seg:not(.kds-mode-seg) .seg-btn {
    /* Equal fifths. `1 1 0` rather than `1 1 auto` so the segments are the same
       width whatever is written in them, which is what makes a segmented
       control read as one control rather than five buttons. */
    flex: 1 1 0;
    min-width: 0;
    padding: 7px 4px;
    font-size: 12.5px;
  }
}

/* Two ways of saying the same range: the full phrase, and the short one a
   phone has room for. Exactly one is ever shown. */
.seg-short { display: none; }
@media (max-width: 560px) {
  .seg-long { display: none; }
  .seg-short { display: inline; }
}

/* A row of controls lines up on their middles. Several toolbars set
   `align-items: flex-end`, which is right for a label-above-a-field and wrong
   for a picker beside a button.

   Only classes the markup actually carries — backoffice-layout.test.js fails a
   rule written for a class nothing uses, and it is right to: a stylesheet full
   of selectors for markup that was renamed years ago is a stylesheet nobody
   can safely change. */
.tbl-bar, .dash-toolbar, .sp-bar, .page-head .row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

/* A select that sits beside a button sizes to its content, not to the row. */
.tbl-bar select, .dash-toolbar select, .sp-bar select, .page-head select {
  width: auto;
  min-width: 8.5rem;
}

/* ---- Two shapes in one bar -----------------------------------------------

   A filter bar holds two kinds of thing: a `.sp-pick` — a label with its
   control underneath, about 69px tall — and a bare button, 40px. Centring both
   in the bar puts the button's middle level with the *stack's* middle, which is
   in the gap between the label text and the field. Measured on the live site:
   Search sat at 165-205 while the three fields it searches sat at 180-220, so
   it was drawn fifteen pixels above the line it belongs on. Products' Clear was
   the same, at 167-203 against 180-220.

   A button in one of these bars is an action on the fields beside it, so it
   lines up with the fields — their bottom edge, which is where a control's own
   line is — and not with the whole stack. Written as `align-self` on the button
   rather than `align-items` on the bar, because the bar also holds a row count
   and a bulk-edit strip that are text and do want centring, and because it then
   holds however the bar itself is aligned. */
.tbl-bar > .btn,
.tbl-bar > button,
.sp-bar > .btn,
.sp-bar > button { align-self: flex-end; }

/* A wrapper that only holds buttons is a button as far as the row is
   concerned, and lines up the same way. */
.tbl-bar > .sp-order,
.sp-bar > .sp-order,
.sp-bar > .sp-surfaces { align-self: flex-end; }

/* ---- One height per bar, taken from the buttons in it --------------------

   Every control in a toolbar should be as tall as the buttons beside it, and
   the two toolbars do not agree on how tall that is: a filter bar above a table
   holds full-size buttons, and the screen editor's bar holds small ones because
   it has eleven controls to fit across a 1000px window. So the height is a
   property of the bar, and the controls read it, rather than every control
   being told a number that is right on one page and wrong on the other.

   Left as it was, Chrome gave a date input 40px and a plain text input 44 under
   the same `min-height` — Department sat two pixels below From and To on the
   Sales Explorer, which is small enough to look like a rendering fault rather
   than a layout one, which is worse. On screen programming the gap was eight:
   Screen, Top bar and Bottom bar were 44px selects against 36px buttons. */
.tbl-bar { --bar-control-h: var(--control-h); }
.sp-bar { --bar-control-h: 36px; }

.tbl-bar .sp-pick > select,
.tbl-bar .sp-pick > input,
.sp-bar .sp-pick > select,
.sp-bar .sp-pick > input {
  height: var(--bar-control-h);
  min-height: var(--bar-control-h);
}

/* A finger is not a mouse pointer, and 36px is a small target for one. Where
   the pointer is coarse every button on the page is already lifted to 40, and
   the controls beside them go with it. */
@media (pointer: coarse) {
  .sp-bar { --bar-control-h: 40px; }
}

/* Kitchen screens: Upload logo and Remove sat in a column of their own beside
   Save branding and Revert, at whatever height their text gave them. The two
   buttons share a row now and the note goes under both, and all four buttons on
   the page are the one height --control-h gives every .btn. */
.rd-logo-actions {
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.rd-logo-actions > p { flex: 1 0 100%; margin: 2px 0 0; }

/* ===========================================================================
   ONE ICON BUTTON
   ===========================================================================
   `.icon-btn` was a second component with its own size, border and marks. It
   is the same thing as `.ibtn`, so the old name is now written into every rule
   the new one has rather than carrying a second definition.
   --------------------------------------------------------------------------- */

/* ===========================================================================
   SCROLLBARS
   ===========================================================================
   The default horizontal bar under a wide table is fifteen pixels of grey
   furniture across a card that is mostly white — on Scheduled reports and
   Timesheets it read as part of the design. Thin, in the brand, and only
   where there is a pointer: a touchscreen has no scrollbar to style and
   overlay bars on a Mac already behave.
   --------------------------------------------------------------------------- */
@media (pointer: fine) {
  .card:has(> table),
  .panel-body,
  .rr-scroll,
  .rail nav,
  .dc-panel,
  .sp-scroll,
  main,
  .modal {
    scrollbar-width: thin;
    scrollbar-color: color-mix(in srgb, var(--brand) 62%, var(--muted)) transparent;
  }

  .card:has(> table)::-webkit-scrollbar,
  .panel-body::-webkit-scrollbar,
  .rr-scroll::-webkit-scrollbar,
  .rail nav::-webkit-scrollbar,
  .dc-panel::-webkit-scrollbar,
  .sp-scroll::-webkit-scrollbar,
  main::-webkit-scrollbar,
  .modal::-webkit-scrollbar { width: 8px; height: 8px; }

  .card:has(> table)::-webkit-scrollbar-track,
  .panel-body::-webkit-scrollbar-track,
  .rr-scroll::-webkit-scrollbar-track,
  .dc-panel::-webkit-scrollbar-track,
  .sp-scroll::-webkit-scrollbar-track,
  main::-webkit-scrollbar-track,
  .modal::-webkit-scrollbar-track { background: transparent; }

  .card:has(> table)::-webkit-scrollbar-thumb,
  .panel-body::-webkit-scrollbar-thumb,
  .rr-scroll::-webkit-scrollbar-thumb,
  .dc-panel::-webkit-scrollbar-thumb,
  .sp-scroll::-webkit-scrollbar-thumb,
  main::-webkit-scrollbar-thumb,
  .modal::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--brand) 55%, var(--muted));
    border-radius: 999px;
  }
  .card:has(> table):hover::-webkit-scrollbar-thumb,
  .panel-body:hover::-webkit-scrollbar-thumb,
  .rr-scroll:hover::-webkit-scrollbar-thumb,
  main:hover::-webkit-scrollbar-thumb { background: var(--brand-deep); }

  /* The corner where two bars meet, which otherwise stays default grey. */
  ::-webkit-scrollbar-corner { background: transparent; }
}

/* ===========================================================================
   THE IDLE SCREEN
   ===========================================================================
   Its sections were laid out as whatever each happened to be — one a grid, one
   a bare stack, one a row — so on a desk they stepped in and out against each
   other down the page. One column, one gap, and each section its own card.
   --------------------------------------------------------------------------- */
/* The view is #view-idle. Its cards sit in a .rd-editor column, and the two
   things that made them step in and out against each other on a desk were:
   cards that did not all start their content at the same place, and a
   two-column pass that let a short card and a tall card share a row with
   different top edges.

   `align-items: start` on the column, one padding on every card, and headings
   that all take the same space, so a section under a section lines up with it. */
#view-idle .rd-editor { align-items: start; }
#view-idle .rd-editor > .card { width: 100%; padding: var(--card-pad); margin: 0; }
#view-idle .rd-card > h3 { margin: 0 0 12px; min-height: 20px; }
#view-idle .rd-card > p.muted { margin-top: 0; }

/* The last control in a card should not sit hard against its bottom edge while
   the card beside it has room to spare. */
#view-idle .rd-card > *:last-child { margin-bottom: 0; }

@media (min-width: 1101px) {
  /* Where the cards do share a row, they share a top edge. */
  #view-idle .rd-layout:not(:has(> * + *)) .rd-editor { align-items: start; }
}

/* ===========================================================================
   CHOOSING A FILE
   ===========================================================================
   The platform's own file input is a grey button with "No file chosen" beside
   it, in the platform's font, and it says nothing afterwards about what was
   picked. This is a label styled as one of our buttons with the input hidden
   inside it, and the name of the file goes next to it once there is one.
   --------------------------------------------------------------------------- */
.filepick { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.filepick input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.filepick label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  height: var(--control-h);
  padding: 0 16px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  color: var(--text);
  font-size: 14px;
  font-weight: 550;
  cursor: pointer;
}
.filepick label:hover { border-color: var(--brand); background: var(--hover); }
.filepick input[type="file"]:focus-visible + label {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
.filepick label svg {
  width: 18px; height: 18px; fill: currentColor;
}
.filepick .picked {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-size: 13.5px;
  color: var(--muted);
}
.filepick .picked b {
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 22rem;
}
.filepick .picked .size { flex: 0 0 auto; }

/* A line under a form control, for a field whose label cannot carry the whole
   answer. Quiet on purpose: it is there for the person meeting the field for
   the first time, and it must not compete with the label above it. */
.field-hint {
  display: block;
  margin-top: 5px;
  font-size: 12px;
  line-height: 1.45;
  font-weight: 400;
  color: var(--muted, #8b93a1);
}

/* The fourteen allergens, ticked.

   Two columns because fourteen in one column is a scroll inside a modal that
   already scrolls, and the labels are short enough to sit side by side. Falls
   to one column on a narrow screen rather than truncating a label — "Cereals
   containing gluten" clipped to "Cereals containi" is worse than a taller
   form. */
.allergen-field {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 14px;
  margin-top: 4px;
}
@media (max-width: 560px) {
  .allergen-field { grid-template-columns: 1fr; }
}
.allergen-field .allergen-tick {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 13px;
  font-weight: 400;
  min-width: 0;
}
.allergen-field .allergen-tick span {
  /* Wraps rather than truncating.

     "Cereals containing gluten" does not fit one column of a 388px modal, and
     it is the FSA's own wording — shortening it to make it fit would be
     rewriting a legal label to suit a layout. An ellipsis is worse still: the
     one allergen a coeliac is looking for, cut off mid-word. So the row grows
     to two lines and the grid absorbs it. */
  overflow-wrap: anywhere;
  line-height: 1.3;
}
.allergen-field .allergen-tick input {
  /* Stops the box shrinking when its label takes two lines. */
  flex: 0 0 auto;
}

/* ---------------------------------------------------------------------------
   Copy keys to other pages
   ---------------------------------------------------------------------------
   The list of pages scrolls rather than growing the dialog: a venue with
   thirty pages would otherwise push Copy off the bottom of the screen, which
   is the one control the dialog exists for.

   The report below it is the part that matters. It stays until the dialog is
   closed, because a manager needs to be able to read which keys a bar refused
   after the toast has gone.
   ------------------------------------------------------------------------ */
.sp-copy-list {
  max-height: 40vh;
  overflow-y: auto;
  border: 1px solid var(--line, #d9dde5);
  border-radius: 8px;
  padding: 8px 10px;
}

.sp-copy-report {
  margin-top: 12px;
  max-height: 30vh;
  overflow-y: auto;
}

.sp-copy-row {
  padding: 6px 0;
  border-top: 1px solid var(--line, #d9dde5);
}

.sp-copy-row ul {
  margin: 4px 0 0;
  padding-left: 18px;
}

/* ---------------------------------------------------------------------------
   Price levels
   ---------------------------------------------------------------------------
   Two columns of rules, a scrolling product list, and a preview table that can
   be four hundred rows long — so both lists scroll inside themselves rather
   than pushing the Apply button somewhere a manager has to hunt for it.
   ------------------------------------------------------------------------ */
.pl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 22px;
}

.pl-products {
  margin-top: 12px;
  max-height: 46vh;
  overflow-y: auto;
  border: 1px solid var(--line, #d9dde5);
  border-radius: 10px;
  padding: 6px 10px;
}

/* A department is a heading you can press, because "all the beers" is the unit
   a venue actually reprices in. */
.pl-dept-head {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line, #d9dde5);
  padding: 10px 2px 6px;
  margin-top: 8px;
  font-weight: 700;
  cursor: pointer;
  color: inherit;
}

.pl-dept-head:hover { color: var(--brand, #6b7f12); }

.pl-item {
  padding: 3px 2px 3px 10px;
}

.pl-preview-table {
  max-height: 44vh;
  overflow-y: auto;
  border: 1px solid var(--line, #d9dde5);
  border-radius: 10px;
}

.pl-skipped { margin-top: 10px; }
.pl-skipped ul { margin: 6px 0 0; padding-left: 18px; }

/* The action rows on this page hold one or two buttons and a note, so the
   buttons take their natural width instead of stretching across the card. */
.view #pl-preview,
.view #pl-apply,
.view #pl-cancel {
  flex: 0 0 auto;
  align-self: flex-start;
}


/* ---------------------------------------------------------------------------
   The gym
   ---------------------------------------------------------------------------
   A board that gets left up on a screen in a room, so it is read from further
   away than anything else in this back office: the row colours are the whole
   point of the page and they carry a word beside them as well. Colour alone
   leaves out the roughly one man in twelve who cannot separate the green from
   the red, and this is exactly the screen — glanced at, from across a room —
   where that matters most. */

.gym-board-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.gym-board-head h3 { margin-bottom: 2px; }

/* Vesopa Express. The tabs, panels and heads are the gym's; these are the few
   things the page has of its own. */
.exp-url {
  display: block;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--surface-2, #eef0f4);
  font-size: 12.5px;
  word-break: break-all;
}
.exp-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0 4px; }
.exp-no { font-variant-numeric: tabular-nums; font-size: 15px; }
.exp-gone td { opacity: .55; }
.exp-sub { margin-top: 16px; }
.exp-picks {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 2px 16px;
}
#view-express .table input[type="text"],
#view-express .table select { width: 100%; min-width: 140px; }
/* Where each kitchen station's paper got to, under the order's status. */
.exp-prints { margin-top: 4px; line-height: 1.9; }
.exp-steps { margin: 6px 0 12px; padding-left: 20px; line-height: 1.6; }
/* Dish, product, size and the button on one line where there is room, and
   stacked on a phone. */
.exp-meal-add {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.4fr) minmax(0, .8fr) auto;
  gap: 10px;
  align-items: end;
}
.exp-meal-add label { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.exp-meal-add select, .exp-meal-add input { width: 100%; min-width: 0; }
@media (max-width: 760px) {
  .exp-meal-add { grid-template-columns: 1fr; }
}

.gym-date,
.gym-range {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--muted);
}
.gym-range label,
.gym-date { display: flex; flex-direction: column; gap: 4px; }
.gym-range input,
.gym-date input { min-width: 140px; }

/* In, and been and gone. A tinted row rather than tinted text: the name is the
   thing being read, and recolouring it would trade legibility for the status. */
.gym-row.is-in { background: var(--ok-wash); }
.gym-row.is-out { background: transparent; }
.gym-row.is-in td { border-bottom-color: transparent; }

.gym-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: var(--muted);
}
.gym-dot.in { background: #17a673; box-shadow: 0 0 0 3px rgba(23, 166, 115, .18); }
.gym-dot.out { background: var(--red, #c0392b); opacity: .55; }

/* A shade stronger than --ok-wash, because this pill sits ON a row already
   tinted with --ok-wash: the two washes cancelled and left green text floating
   with no pill around it. */
.pill.green { background: rgba(23, 166, 115, .20); color: #06784f; }
.pill.danger { background: var(--bad-wash); color: var(--red, #c0392b); }

/* When the room is busy: twenty-four small numbers, drawn as bars rather than
   pulled into a chart library that would be more code than the thing it draws. */
.gym-hours {
  display: grid;
  grid-template-columns: repeat(24, 1fr);
  gap: 4px;
  align-items: end;
  margin-top: 8px;
}
.gym-hour { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.gym-hour-bar {
  width: 100%;
  height: 90px;
  display: flex;
  align-items: flex-end;
  background: var(--track);
  border-radius: 4px;
  overflow: hidden;
}
.gym-hour-bar i {
  display: block;
  width: 100%;
  background: var(--brand, #6c4cd6);
  border-radius: 4px 4px 0 0;
  min-height: 2px;
}
.gym-hour-label { font-size: 10px; color: var(--muted); }
.gym-hour-count { font-size: 10.5px; font-weight: 600; }

/* Narrow screens cannot show twenty-four labels without them colliding, so
   every other one goes. The bar is still there; only its number is dropped. */
@media (max-width: 720px) {
  .gym-hour:nth-child(even) .gym-hour-label,
  .gym-hour-count { display: none; }
  .gym-hour-bar { height: 64px; }
}

/* What the door will do, spelled out beside the boxes that decide it. */
.gym-rules {
  margin: 12px 0 0;
  padding-left: 18px;
  font-size: 12.5px;
  color: var(--muted);
  line-height: 1.6;
}
.gym-rules b { color: var(--text); font-weight: 600; }

/* ---- Loyalty app ---------------------------------------------------------- */
.la-check { display: flex; align-items: center; gap: 8px; font-weight: 600; }
.la-slug { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.la-slug input { flex: 1; min-width: 160px; }
.la-pictures, .la-colours, .la-fonts, .la-links, .la-where, .la-audience {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px 14px;
  margin: 8px 0;
}
.la-colours input[type=color] { width: 100%; height: 38px; padding: 2px; border-radius: 8px; }
.la-phone { display: flex; justify-content: center; padding: 8px 0 16px; }
.la-screen {
  width: 260px; min-height: 480px; border-radius: 28px; overflow: hidden;
  border: 8px solid #1b1d22; box-shadow: 0 12px 30px rgba(0,0,0,.18);
  display: flex; flex-direction: column; font-family: Montserrat, system-ui, sans-serif;
}
.la-top { display: flex; align-items: center; gap: 10px; padding: 14px; color: #fff; }
.la-top img { width: 34px; height: 34px; border-radius: 8px; object-fit: cover; background: #fff; }
.la-card {
  margin: 14px; padding: 14px; border-radius: 16px; background: #fff; color: #111;
  border: 3px solid; text-align: center; box-shadow: 0 6px 16px rgba(0,0,0,.08);
}
.la-card-name { font-weight: 700; margin-bottom: 8px; }
.la-qr-fake {
  width: 120px; height: 120px; margin: 0 auto 6px; border-radius: 6px;
  background:
    repeating-linear-gradient(90deg, #111 0 6px, transparent 6px 12px),
    repeating-linear-gradient(0deg, #111 0 6px, #fff 6px 12px);
  opacity: .85;
}
.la-points { margin-top: 8px; font-size: 18px; }
.la-screen > p { padding: 0 16px; }
.la-tabs {
  margin-top: auto; display: flex; justify-content: space-around; padding: 12px 8px;
  background: rgba(0,0,0,.04); font-size: 12px; font-weight: 600;
}
.la-poster #la-qr svg { width: 180px; height: 180px; display: block; margin: 0 auto; }

/* The practice venue's banner.
 *
 * In the rail, so it is on every page. Amber and not red: nothing is wrong, but
 * nothing here is real either, and the one mistake this feature could cause is
 * a manager reading practice takings as the day's. It is deliberately the
 * loudest thing in the rail. */
.demo-banner {
  margin: 0 12px 10px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-radius: 8px;
  border: 1px solid #b45309;
  background: #78350f;
  color: #fef3c7;
}
.demo-banner strong {
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.demo-banner span {
  font-size: 12px;
  line-height: 1.35;
  color: #fde68a;
}
.demo-banner .btn {
  width: 100%;
  justify-content: center;
}
