/* pagerlab: tokens and components. The reasons live in DESIGN_NOTES.md.
   The look is the café pager in the customer's hand: a round moulded buzzer in the venue's
   colour, a dark channel of amber lamps and a light face with the number, on plain paper.
   Rules: logical properties only, so dir="rtl" needs no extra CSS beyond number alignment.
   No words sit on the venue's colour, so any brand colour is legible: the number stays on the
   face and the words on the paper or the band. */

/* 1. Raw palettes. Warm: karak paper and date ink. */
:root {
  --l-paper: #FFE9CC; --l-field: #FFF7EA; --l-fill: #F7D4A6;
  --l-ink: #200D05;   --l-ink-2: #65391C; --l-line: #8F5E36;
  --d-paper: #1E0E07; --d-field: #2C1A10; --d-fill: #3D2618;
  --d-ink: #FFEAD0;   --d-ink-2: #E0BE9C; --d-line: #A9825F;
  /* Ready's ground, under the flood: the far end of each palette, for the 17:1 change */
  --l-ground: #FFF7EA; --d-ground: #0D0502;
  /* The buzzer, the same on the paper and at Ready: the channel is always dark, so the lamps read on any
     body colour. The body is the venue's colour, pomegranate where a venue has none: the
     template sets these two for a venue with a colour (app.venue_accents, app.venue_floods). */
  --channel: #1A0A03; --lamp-on: #FFB52E; --lamp-off: #57331E; --alarm: #FF5A3C;
  --puck-l: #A81F3A; --flood-l: #AF2740;

  --font: "Rubik", system-ui, sans-serif;
  /* Type scale, ratio 1.25 from a 17px body. Nothing a customer reads is smaller than step-0. */
  --step-0: 1.0625rem; --step-1: 1.3125rem; --step-2: 1.625rem;
  --step-3: 2.0625rem; --step-4: 2.5625rem;
  --num-wait: 4.75rem;    /* order number while preparing, at most */
  --num-ready: 11rem;     /* order number once ready, at most */
  --ready-word: clamp(3rem, 16vw, 4.5rem);
  --ar-scale: 1.0625;                        /* Arabic beside Latin at matched visual size */

  --s1: 0.25rem; --s2: 0.5rem; --s3: 0.75rem; --s4: 1rem; --s5: 1.5rem; --s6: 2rem; --s7: 3rem;
  --gutter: 1rem;
  --radius: 1rem;
  --tap: 3.5rem;          /* 56px: every customer button */
  --tap-staff: 4rem;      /* 64px: Ready and Collected on the tablet */
}

/* 2. Semantic tokens. Every phone shows the light palette, the one in the demo video, whatever
   its dark-mode setting (the founder's choice, 9 Nov 2026): a phone in dark mode no longer turns
   the page brown. "only light" also keeps Chrome's automatic dark mode off the page. Ready takes
   the dark palette, so it still flips. */
:root {
  color-scheme: only light;
  --bg: var(--l-paper); --fg: var(--l-ink); --fg-2: var(--l-ink-2);
  --field: var(--l-field); --fill: var(--l-fill); --line: var(--l-line);
  --ground: var(--l-ground); --face: var(--l-field);
  --plate: transparent;
}
.status[data-state="ready"] {
  color-scheme: dark;
  --bg: var(--d-paper); --fg: var(--d-ink); --fg-2: var(--d-ink-2);
  --field: var(--d-field); --fill: var(--d-fill); --line: var(--d-line);
  --ground: var(--d-ground); --face: var(--d-field);
  --plate: var(--l-paper);
}
/* The venue's colour: the pager's body, and Ready's flood. Words never sit on either, and Ready's
   opposite palette leaves them alone. */
:root { --puck: var(--puck-l); --flood: var(--flood-l); }

/* 3. Base */
*, *::before, *::after { box-sizing: border-box; }
html { font-size: 100%; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
html:lang(ar) { font-size: calc(100% * var(--ar-scale)); }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font-family: var(--font); font-size: var(--step-0); line-height: 1.5;
  -webkit-tap-highlight-color: transparent;
}
html:lang(ar) body { line-height: 1.7; }
h1, h2, p, ol, ul { margin: 0; }
h1, h2 { font-size: inherit; font-weight: inherit; }
ol, ul { padding: 0; list-style: none; }
p { max-inline-size: 65ch; }
a { color: inherit; text-underline-offset: 0.2em; }
:focus-visible { outline: 3px solid var(--fg); outline-offset: 3px; }

/* 4. Page frame: one column on the phone, wide on staff screens */
.page {
  display: flex; flex-direction: column; gap: var(--s4);
  max-inline-size: 30rem; min-block-size: 100vh; min-block-size: 100svh;
  margin-inline: auto; padding: 0 var(--gutter) var(--s5);
}
.page:has(.staff-board), .page:has(.login) { gap: var(--s3); max-inline-size: 75rem; --gutter: 1.5rem; }

/* 5. Venue header */
.venue {
  display: flex; align-items: center; gap: var(--s3);
  min-block-size: 4.5rem; padding-block: var(--s3);
  font-size: var(--step-1); font-weight: 600; line-height: 1.2;
}
.venue img {
  display: block; block-size: 3rem; inline-size: auto; max-inline-size: 9rem; min-inline-size: 0;
  object-fit: contain; padding: var(--s1); border-radius: 0.625rem; background: var(--plate);
}
/* A wide logo and a long name share the row: the logo gives way before the name's longest word,
   and the cap stops a name with no spaces. */
.venue h1 { overflow-wrap: break-word; max-inline-size: calc(100% - 8rem); }
/* Safari, first visit: it sized a Latin name on an Arabic page with kerning and set the line
   without, so "Roastery" broke before its last letter, and it stayed broken. */
html:lang(ar) .venue h1 { font-kerning: none; }
.page:has(.login) > .venue:not(:has(img)) > h1 { max-inline-size: calc(100% - 9.25rem); }   /* beside the small pager */
@media (min-width: 30rem) {   /* room for Sign out as well */
  :is(.page:has(.staff-board), .admin) > .venue > h1 { max-inline-size: calc(100% - 16rem); }
}
@media (max-width: 29.99rem) {   /* the owner's pages wrap as the board does (section 11) */
  main.page.admin { --gutter: 1rem; }
  .admin > .venue { flex-wrap: wrap; justify-content: flex-end; row-gap: var(--s2); }
  .admin > .venue > h1 { margin-inline-end: auto; max-inline-size: calc(100% - 9rem - var(--s3)); }
  .admin > .venue:not(:has(img)) > h1 { max-inline-size: calc(100% - 1.75rem - var(--s3)); }
}
.venue > :is(.lang-toggle, .btn) { margin-inline-start: auto; }
.venue > :is(.lang-toggle, .btn) ~ :is(.lang-toggle, .btn) { margin-inline-start: 0; }
.venue > .btn { min-block-size: 2.75rem; padding-inline: var(--s4); font-size: var(--step-0); }
/* Staff and owner pages have no pager, so the venue's colour sits by its name as a small one,
   unless the logo is there to say whose page it is: ring, channel and face, as on the tab icon. */
:is(.page:has(.staff-board), .page:has(.login), .admin, .qr-overlay) .venue:not(:has(img))::before {
  content: ""; flex: none; inline-size: 1.75rem; block-size: 1.75rem; border-radius: 50%;
  background: var(--face); box-shadow: inset 0 0 0 0.1875rem var(--puck), inset 0 0 0 0.5rem var(--channel);
}

.lang-toggle {
  display: inline-flex; align-items: center; min-block-size: 2.75rem; padding-inline: var(--s4);
  border-radius: 1.375rem; background: var(--fill); color: var(--fg);
  font-size: var(--step-0); font-weight: 600; text-decoration: none; white-space: nowrap;
}

/* 6. Bilingual pairs: English and Arabic at matched size, each flush to its own reading start.
   Used for every status title and on the QR overlay. Children carry lang="en" / lang="ar". */
:is(.status-title, .pair) { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--s4); }
:is(.status-title, .pair) > [lang] { direction: inherit; unicode-bidi: isolate; }
:is(.status-title, .pair) > [lang] + [lang] { margin-inline-start: auto; }
html:not(:lang(ar)) :is(.status-title, .pair) > :lang(ar) { font-size: calc(1em * var(--ar-scale)); }
html:lang(ar) :is(.status-title, .pair) > :lang(en) { font-size: calc(1em / var(--ar-scale)); }
:is(.status-title, .pair) > :lang(ar) { line-height: 1.35; }

/* 7. Fields (staff sign-in and the Add row) and short info pages */
.field { display: grid; gap: var(--s2); }
.field > label { font-weight: 600; }
.field > p:not(.notice) { color: var(--fg-2); }
.info { display: grid; gap: var(--s3); }
.info > p { color: var(--fg-2); }
/* The privacy notice reads as text, in ink; the quiet link to it sits at the foot of the page */
.privacy > p { color: var(--fg); }
.page-foot { margin-block-start: auto; color: var(--fg-2); }
/* A number field is a plain field with the number at its full weight. Focus takes the page's ring
   and an ink rim, and an error a thicker rim, so neither state rests on colour alone. */
.input-number {
  inline-size: 100%; block-size: 4.5rem; margin: 0; padding-inline: var(--s4);
  border: 2px solid var(--line); border-radius: var(--radius);
  background: var(--field); color: var(--fg); caret-color: var(--fg);
  font: 800 var(--step-4)/1 var(--font); letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums lining-nums;
  direction: ltr; text-align: left; appearance: none;
}
[dir="rtl"] .input-number { text-align: right; }
.input-number:focus { border-color: var(--fg); }
.input-number[aria-invalid="true"] { padding-inline: calc(var(--s4) - 1px); border-width: 3px; border-color: var(--fg); }
.notice { font-weight: 600; color: var(--fg); }
.notice:empty { display: none; }

/* 8. Buttons: moulded keys, like the pager's own. Pressing one sinks it onto its edge. */
.btn {
  --edge: var(--line);
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-block-size: var(--tap); margin: 0 0 0.25rem; padding: var(--s2) var(--s5);
  border: 0; border-radius: var(--radius); background: var(--fill); color: var(--fg);
  box-shadow: 0 0.25rem 0 var(--edge);
  font: 600 var(--step-1)/1.2 var(--font); text-align: center; text-decoration: none;
  cursor: pointer; touch-action: manipulation; transition: transform 80ms, box-shadow 80ms;
}
.btn:active { transform: translateY(0.25rem); box-shadow: 0 0 0 var(--edge); }
.btn-primary, .btn-ready { --edge: var(--puck); background: var(--fg); color: var(--bg); }
.btn-quiet, .btn-collected { background: var(--fill); color: var(--fg); }
@supports (color: color-mix(in oklab, red, blue)) {
  .btn-primary:active, .btn-ready:active { background: color-mix(in oklab, var(--fg) 80%, var(--bg)); }
  .btn-quiet:active, .btn-collected:active { background: color-mix(in oklab, var(--fill) 82%, var(--fg)); }
  @media (hover: hover) {
    .btn-primary:hover, .btn-ready:hover { background: color-mix(in oklab, var(--fg) 88%, var(--bg)); }
    .btn-quiet:hover, .btn-collected:hover { background: color-mix(in oklab, var(--fill) 90%, var(--fg)); }
  }
}
.btn:disabled, .btn[aria-busy="true"] { opacity: 0.55; cursor: progress; }
.status-actions .btn { inline-size: 100%; }

/* 9. Customer status. data-state = preparing | washing (a car wash, M4) | ready | collected | expired
   | replaced (Replace phone moved the order to another phone)
   Every size on the pager is a multiple of --u, a hundredth of its width, so it scales as one
   object with the room the screen leaves. Only the fit below uses container units, inside its
   @supports: the device itself draws on any phone. */
.status { display: flex; flex-direction: column; gap: var(--s4); flex: 1; color: var(--fg); }
.status-words { display: flex; flex-direction: column; gap: var(--s3); }
.status-title { font-size: 1.75rem; font-weight: 800; line-height: 1.2; }
.status-queue { font-size: var(--step-2); font-weight: 600; line-height: 1.25; text-wrap: balance; }
html:lang(ar) .status-queue { line-height: 1.35; }   /* a tail on one line must clear the dots on the next */
.status-lead { font-size: var(--step-1); line-height: 1.35; text-wrap: pretty; }
.status-body, .status-step { color: var(--fg-2); text-wrap: pretty; }
.status-actions { display: grid; gap: var(--s3); margin-block-start: var(--s3); }
/* The step and the Sound on lines share one cell, and Turn on sound and Sound on the row under it,
   so the block keeps the taller line's height when they swap: the fold gives no room back to the
   buzzer at the tap, and the words above never move (section 6). A line that is off keeps its
   place unseen. Without :has() the lines swap as before. */
.status-actions:has(> #sound-step) > .status-step { grid-area: 1 / 1; }
.status-actions:has(> #sound-step) > .btn { grid-area: 2 / 1; }
.status-actions:has(> #sound-step) > .status-step[hidden] { display: block !important; visibility: hidden; }
/* Sound on: the key stays down, in the place it was tapped, with its lamp lit. The lamp sits in a
   ring of the pager's channel, so it reads on the light key as it does on the dark one. */
.btn-on { transform: translateY(0.25rem); box-shadow: none; cursor: default; }
.btn-on::before {
  content: ""; inline-size: 0.75rem; block-size: 0.75rem; margin-inline-end: 0.1875rem; border-radius: 50%;
  background: var(--lamp-on); box-shadow: 0 0 0 0.1875rem var(--channel), 0 0 0.5rem 0.1875rem var(--lamp-on);
}
/* Alarm stopped: the same key down, its lamp out. */
.btn-off::before { background: var(--lamp-off); box-shadow: 0 0 0 0.1875rem var(--channel); }

/* The pager. --pager-size is the room the page gives it; --n is the lamp count (the inline style:
   the venue's cap and one for Ready). */
.status { --pager-size: min(100vw - 2 * var(--gutter), 18.75rem); }
.status-fold { display: flex; flex: none; flex-direction: column; gap: var(--s4); }
.status:is([data-state="preparing"], [data-state="washing"]) > .status-fold {
  --pager-size: min(100vw - 2 * var(--gutter), 18.75rem, max(10rem, 100vh - 30rem));
}
@supports (height: 100svh) {
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold {
    --pager-size: min(100vw - 2 * var(--gutter), 18.75rem, max(10rem, 100svh - 30rem));
  }
}
.pager-mark { --pager-size: 7rem; margin-block-end: var(--s3); }
.pager-mark .pager { cursor: auto; }
.pager-stage { position: relative; display: grid; place-items: center; }
.pager {
  --width: var(--pager-size); --u: calc(var(--width) / 100);
  position: relative; display: grid; place-items: center; isolation: isolate; cursor: pointer;
  inline-size: var(--width); block-size: var(--width);
  /* Held in the hand: a long press would select the number and put Copy and Look Up over the
     face (iPhone Safari), so nothing on the buzzer is selected or called out. */
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
/* A number of 5 or 6 characters keeps a pager wide enough for it. */
.pager-wide { --width: min(100vw - 2 * var(--gutter), max(var(--pager-size), 18.6rem)); }
/* The body: moulded, lit from above, resting on the table. */
.pager::before {
  content: ""; position: absolute; inset: 0; z-index: -2; border-radius: 50%; background: var(--puck);
  box-shadow: inset 0 calc(0.8 * var(--u)) 0 rgb(255 255 255 / 0.28), inset 0 calc(-1.6 * var(--u)) 0 rgb(0 0 0 / 0.24),
              0 var(--u) calc(1.6 * var(--u)) rgb(60 20 0 / 0.3),
              0 calc(5 * var(--u)) calc(8 * var(--u)) calc(-2 * var(--u)) rgb(60 20 0 / 0.5);
}
/* The channel the lamps sit in, always dark. */
.pager::after {
  content: ""; position: absolute; inset: calc(4.2 * var(--u)); z-index: -1; border-radius: 50%; background: var(--channel);
  box-shadow: inset 0 calc(0.8 * var(--u)) calc(1.4 * var(--u)) rgb(0 0 0 / 0.7);
}
/* Lamps go round the channel, clockwise from the top in both languages. */
.pager-lamps { position: absolute; inset: 0; --lamp: calc(6.5 * var(--u)); }
.lamp {
  position: absolute; inset-block-start: 50%; inset-inline-start: 50%;
  inline-size: var(--lamp); block-size: var(--lamp); margin: calc(var(--lamp) / -2);
  border-radius: 50%; background: var(--lamp-off);
  transform: rotate(calc(var(--i, 0) * 1turn / var(--n, 6))) translateY(calc(-41.4 * var(--u)));
}
.lamp:nth-child(2) { --i: 1; } .lamp:nth-child(3) { --i: 2; } .lamp:nth-child(4) { --i: 3; }
.lamp:nth-child(5) { --i: 4; } .lamp:nth-child(6) { --i: 5; } .lamp:nth-child(7) { --i: 6; }
.lamp:nth-child(8) { --i: 7; } .lamp:nth-child(9) { --i: 8; } .lamp:nth-child(10) { --i: 9; }
.lamp:nth-child(11) { --i: 10; } .lamp:nth-child(12) { --i: 11; } .lamp:nth-child(13) { --i: 12; }
.lamp:nth-child(14) { --i: 13; } .lamp:nth-child(15) { --i: 14; } .lamp:nth-child(16) { --i: 15; }
.lamp:nth-child(17) { --i: 16; } .lamp:nth-child(18) { --i: 17; } .lamp:nth-child(19) { --i: 18; }
.lamp:nth-child(20) { --i: 19; } .lamp:nth-child(21) { --i: 20; }
.lamp::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%; opacity: 0;
  background: var(--lamp-on); box-shadow: 0 0 calc(2.4 * var(--u)) calc(0.2 * var(--u)) var(--lamp-on);
}
.lamp.on::after { opacity: 1; }
.lamp.next::after { animation: lamp-breathe 2.8s ease-in-out infinite; }
/* The face: the number's plate, the same whatever the body's colour. --face-w is its width. */
.status-number, .pager-face {
  --face-w: calc(73 * var(--u));
  position: relative; display: grid; justify-items: center; align-content: center; text-align: center;
  inline-size: var(--face-w); block-size: var(--face-w); max-inline-size: none;
  border-radius: 50%; background: var(--face); box-shadow: 0 calc(0.6 * var(--u)) calc(1.2 * var(--u)) rgb(0 0 0 / 0.55);
  font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums lining-nums; overflow-wrap: anywhere;
}
/* The face is round: the number's width keeps a long one inside, the last limit a short one, whose
   corners would otherwise reach the dark channel on a small face. One line, as app.number_width
   assumes: WebKit breaks a plate at its hyphen on an Arabic page, and the plate would grow over the
   title. */
.status-number > bdi { white-space: nowrap; }
.status-number > bdi { font-size: min(var(--num-wait), var(--face-w) * 0.9 / (0.69 * var(--chars, 4)), var(--face-w) * 0.4); }
.status-number small {
  display: block; margin-block-end: var(--s1); color: var(--fg-2);
  font-size: max(var(--step-0), var(--face-w) * 0.08); font-weight: 600; line-height: 1.3;
}
/* The wide plate (.pager-wide is the round buzzer at five or six digits). The number keeps its size, on a face as wide as the screen
   allows, with the lamps in a row under it. */
.pager-slab { --width: calc(min(100vw, 30rem) - 2 * var(--gutter)); block-size: auto; padding: var(--s3) var(--s3) 2.5rem; }
.pager-slab::before { border-radius: 2.25rem; }
.pager-slab::after { inset: auto var(--s3) 0.625rem; block-size: 1.5rem; border-radius: 0.75rem; }
.pager-slab .pager-lamps {
  inset: auto 1.25rem 0.875rem; block-size: 1rem; display: flex; justify-content: space-between;
  direction: ltr; --lamp: 0.75rem;
}
.pager-slab .lamp { position: relative; inset: auto; margin: 0.125rem 0 0; transform: none; }
.pager-slab .status-number {
  --face-w: calc(var(--width) - 2 * var(--s3));
  block-size: auto; border-radius: 1.625rem; padding-block: var(--s3);
}
.pager-slab .status-number > bdi { font-size: min(var(--num-wait), var(--face-w) * 0.92 / (0.69 * var(--chars, 4))); }
.pager-slab .status-number small { font-size: var(--step-0); }
/* An order ahead left: the pager hops once (status.js sets data-tick). */
.status[data-tick] .pager { animation: pager-hop 420ms ease-out; }
/* An ended order: the pager at rest, smaller, its lamps gone (the template draws none). */
.status:is([data-state="collected"], [data-state="expired"], [data-state="replaced"]) { --pager-size: 11rem; }
.status:is([data-state="collected"], [data-state="expired"], [data-state="replaced"]) .pager-lamps { display: none; }
.status:is([data-state="collected"], [data-state="expired"], [data-state="replaced"]) .pager-slab { padding-block-end: var(--s3); }
.status:is([data-state="collected"], [data-state="expired"], [data-state="replaced"]) .pager-slab::after { content: none; }
/* While the order waits Turn on sound stays on the first screen, whatever the words come to: the
   fold is one screen, the header and the words keep their height, and the pager takes what is
   left, up to its full size. A name stops at three lines, two on a short screen: the page's title
   keeps all of it. It takes its own direction, so a Latin name on an Arabic page ends in its dots
   at its own end. Leave its alignment to that direction: set against it, the line is aligned
   before the dots are added, and they fall outside the box, so a cut name reads as a whole one. */
.status-fold > .venue > h1 {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
  unicode-bidi: plaintext;
}
@supports (width: 1cqmin) and (height: 100svh) {
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold { max-block-size: calc(100svh - var(--s3)); }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > :is(.venue, .status-words) { flex: none; }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage { flex: 0 1 18.75rem; min-block-size: 6rem; container-type: size; }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage-wide { min-block-size: 7.25rem; }   /* 6 digits stay at step-0 */
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage-slab { flex-basis: 11.25rem; min-block-size: 6.5rem; }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage:not(.pager-stage-slab) > .pager { --width: min(100cqi, 100cqb); }
  /* The long plate has three heights for the room it gets: its full size, closer in with a
     smaller number, and that without its caption. The round pager drops its caption at its smallest. */
  @container (max-block-size: 11.2rem) {
    .status:is([data-state="preparing"], [data-state="washing"]) .pager-slab { padding: var(--s2) var(--s2) 2.25rem; }
    .status:is([data-state="preparing"], [data-state="washing"]) .pager-slab .status-number { --face-w: calc(var(--width) - 2 * var(--s2)); padding-block: var(--s1); }
    .status:is([data-state="preparing"], [data-state="washing"]) .pager-slab .status-number > bdi { font-size: min(3.25rem, var(--face-w) * 0.92 / (0.69 * var(--chars, 4))); }
  }
  @container (max-block-size: 8.1rem) {
    .status:is([data-state="preparing"], [data-state="washing"]) .pager-slab .status-number small { display: none; }
  }
  @container (max-block-size: 8.75rem) {
    .status:is([data-state="preparing"], [data-state="washing"]) .pager:not(.pager-slab) .status-number small { display: none; }
  }
  /* A phone with its browser's bars showing: the fold is at least the screen and the round pager
     grows into it from its smallest size, so words that need more than the screen make the page
     longer and it scrolls: the fold never runs over what follows it. The long plate is short
     enough to keep the first way, with what follows it on the screen, down to the shortest phones. */
  @media (max-height: 46rem) {
    .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold:not(:has(> .pager-stage-slab)) { max-block-size: none; min-block-size: calc(100svh - var(--s3)); }
    .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage:not(.pager-stage-slab) { flex: 1 1 6rem; max-block-size: 18.75rem; }
    .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage-wide { flex-basis: 7.25rem; }
  }
  @media (max-height: 36rem) {
    .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold { max-block-size: none; min-block-size: calc(100svh - var(--s3)); }
    .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .pager-stage-slab { flex: 1 1 6.5rem; max-block-size: 11.25rem; }
  }
}
/* The same phone: the header and the gaps give way first, then the words, and on the shortest
   screens the kind line drops under the key. */
@media (max-height: 46rem) {
  .status-fold > .venue { min-block-size: 3.5rem; padding-block: var(--s1); }
  .status-fold > .venue > h1 { -webkit-line-clamp: 2; }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold { gap: var(--s3); }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold > .status-words { gap: var(--s2); }
  .status:is([data-state="preparing"], [data-state="washing"]) > .status-fold .status-actions { margin-block-start: var(--s1); }
  html:lang(ar) .status-fold .status-step { line-height: 1.5; }
}
@media (max-height: 40rem) {
  .status:is([data-state="preparing"], [data-state="washing"]) .status-queue { font-size: var(--step-1); }
  .status:is([data-state="preparing"], [data-state="washing"]) .status-lead { font-size: var(--step-0); }
}
@media (max-height: 36rem) {
  .status:is([data-state="preparing"], [data-state="washing"]) .status-lead { order: 1; }
}

/* Ready: the pager comes up to fill the screen. Every 0.6 s (0.83 flashes a second) the venue's
   colour floods the screen and the word's band lights, then both give way to the ground. The
   band flips on any brand, so the flash holds even where the flood is dark. The number stays on
   its face, in the same colours, so nothing that must be read moves. Words, size, luminance,
   motion, sound and vibration all change at once: no cue depends on hue.
   Ready runs under the notch and the Dynamic Island, so on a phone on its side one of them sits
   at an end of the screen: --side keeps the words, and --plate-side the long plate, clear of the
   side safe areas (the same on both sides, as the plate is centred), while the flood and the band
   still run to the edges. Upright, both insets are 0. */
.status[data-state="ready"] {
  position: fixed; inset: 0; z-index: 10; overflow: hidden auto; isolation: isolate; gap: 0;
  background: var(--ground);
  --pager-size: min(125vw, max(20rem, 100vh - 15.75rem));
  --side: max(var(--gutter), env(safe-area-inset-left), env(safe-area-inset-right));
  --plate-side: max(var(--s2), env(safe-area-inset-left), env(safe-area-inset-right));
}
@supports (height: 100svh) {
  .status[data-state="ready"] { --pager-size: min(125vw, max(20rem, 100svh - 15.75rem)); }
}
/* Two drawn frames after Ready ends, the box unpainted and its children on screen: Safari 26 keeps
   Ready's colour in its bars without this (status.js, releaseBars). Outside Ready the box paints
   nothing, so no page shows it. */
.status[data-bars] { visibility: hidden; }
.status[data-bars] > * { visibility: visible; }
.status[data-state="ready"]::before {
  content: ""; position: fixed; inset: 0; z-index: -3; background: var(--flood);
  animation: flood 1.2s linear infinite;
}
.status[data-state="ready"][data-alarm="off"]::before { animation: none; }
.status[data-state="ready"] .status-fold { display: contents; }
.status[data-state="ready"] .venue { display: none; }
.status[data-state="ready"] .pager-stage {
  flex: 1 0 auto; padding-block-start: max(var(--s3), env(safe-area-inset-top));
  overflow: visible; min-inline-size: 0;
}
.status[data-state="ready"] .pager:not(.pager-slab) { --width: var(--pager-size); margin-inline: calc((100vw - var(--pager-size)) / 2); }
.status[data-state="ready"] .status-words {
  flex: none; gap: var(--s3); padding: 0 var(--side) max(var(--s4), env(safe-area-inset-bottom));
  background: var(--ground); color: var(--fg);
}
.status[data-state="ready"] .status-title {
  margin-inline: calc(-1 * var(--side)); padding: var(--s5) var(--side); background: var(--fg); color: var(--bg);
  font-size: var(--ready-word); line-height: 1.1; flex-wrap: nowrap; column-gap: var(--s2);
  animation: board-flip 1.2s step-end infinite;
}
.status[data-state="ready"][data-alarm="off"] .status-title { animation: none; }
.status[data-car-wash] { --ready-word: clamp(3rem, 15vw, 4.5rem); }   /* جاهزة is longer: the pair keeps its gutter at 360 wide */
html:lang(ar) .status[data-car-wash] { --ready-word: clamp(2.75rem, 15vw, 4.5rem); }   /* an Arabic rem is 17px */
html:not(:lang(ar)) .status[data-state="ready"] .status-title > :lang(ar) { font-size: 1em; }
html:lang(ar) .status[data-state="ready"] .status-title > :lang(en) { font-size: 1em; }
.status[data-state="ready"] .status-body { color: var(--fg); font-size: clamp(var(--step-1), 6.4vw, var(--step-2)); font-weight: 600; line-height: 1.25; }
.status[data-state="ready"] .status-actions { margin-block-start: var(--s2); }
.status[data-state="ready"] .pager::before {
  background: var(--flood); box-shadow: none; filter: brightness(0.8);
  animation: flood 1.2s linear infinite;
}
.status[data-state="ready"][data-alarm="off"] .pager::before { animation: none; }
.status[data-state="ready"] .status-number { --face-w: calc(84 * var(--u)); background: var(--ground); box-shadow: none; }
.status[data-state="ready"] .status-number > bdi { font-size: min(var(--num-ready), var(--face-w) * 0.92 / (0.69 * var(--chars, 4)), var(--face-w) * 0.55); }
.status[data-state="ready"] .pager-lamps { --lamp: calc(3.8 * var(--u)); }
.status[data-state="ready"] .lamp { transform: rotate(calc(var(--i, 0) * 1turn / var(--n, 6))) translateY(calc(-46 * var(--u))); }
.status[data-state="ready"] .lamp::after {
  opacity: 1; background: var(--alarm); box-shadow: 0 0 calc(3 * var(--u)) calc(0.4 * var(--u)) var(--alarm);
  animation: lamp-blink 1.2s linear infinite;
}
.status[data-state="ready"]:not([data-alarm="off"]) .pager::after,
.status[data-state="ready"]:not([data-alarm="off"]) .pager-lamps { animation: pager-buzz 2.4s linear infinite; }
.status[data-state="ready"][data-alarm="off"] .lamp::after { animation: none; }
.status[data-state="ready"] .pager-slab {
  --width: calc(100vw - 2 * var(--plate-side)); block-size: auto; margin-inline: 0; padding: var(--s2) var(--s2) 2.25rem;
}
.status[data-state="ready"] .pager-slab .status-number { --face-w: calc(var(--width) - 2 * var(--s2)); block-size: auto; border-radius: 1.75rem; padding-block: var(--s4); }
.status[data-state="ready"] .pager-slab .status-number > bdi { font-size: min(var(--num-ready), var(--face-w) * 0.96 / (0.69 * var(--chars, 4))); }
.status[data-state="ready"] .pager-slab .status-number small { font-size: var(--step-1); }
.status[data-state="ready"] .pager-slab .pager-lamps { --lamp: 0.75rem; }
.status[data-state="ready"] .pager-slab .lamp { transform: none; }
/* The words keep their height, so Stop alarm stays on the screen, and the pager takes the rest. */
@supports (width: 1cqmin) {
  @media (min-height: 35.71rem) {
    .status[data-state="ready"] .pager-stage:not(.pager-stage-slab) { flex: 1 1 0; container-type: size; }
    .status[data-state="ready"] .pager:not(.pager-slab) { --pager-size: min(125cqi, 100cqb); }
  }
}
/* A screen too short for the round pager takes the wide one, so the number keeps its size. */
@media (max-height: 35.7rem) {
  .status[data-state="ready"] .pager-stage > .pager {
    --width: calc(100vw - 2 * var(--plate-side)); block-size: auto; margin-inline: 0; padding: var(--s2) var(--s2) 2.25rem;
  }
  .status[data-state="ready"] .pager::before { border-radius: 2.25rem; }
  .status[data-state="ready"] .pager::after { inset: auto var(--s3) 0.625rem; block-size: 1.5rem; border-radius: 0.75rem; }
  .status[data-state="ready"] .pager .pager-lamps { inset: auto 1.25rem 0.875rem; block-size: 1rem; display: flex; justify-content: space-between; direction: ltr; --lamp: 0.75rem; }
  .status[data-state="ready"] .pager .lamp { position: relative; inset: auto; margin: 0.125rem 0 0; transform: none; }
  .status[data-state="ready"] .pager .status-number { --face-w: calc(var(--width) - 2 * var(--s2)); block-size: auto; border-radius: 1.75rem; padding-block: var(--s2); }
  .status[data-state="ready"] .pager .status-number > bdi { font-size: min(var(--num-ready), var(--face-w) * 0.96 / (0.69 * var(--chars, 4))); }
  .status[data-state="ready"] { --num-ready: 7rem; }
  @supports (height: 100svh) {
    .status[data-state="ready"] { --num-ready: clamp(4rem, 100svh - 26.5rem, 11rem); }
  }
}
/* A phone on its side: the words close up and the caption goes, so Stop alarm stays on the screen,
   and the number takes the height that is left. */
@media (max-height: 30rem) {
  .status[data-state="ready"] { --num-ready: 5.5rem; }
  @supports (height: 100svh) {
    .status[data-state="ready"] { --num-ready: clamp(3rem, 100svh - 17rem, 5.5rem); }
  }
  .status[data-state="ready"] .pager-stage { padding-block-start: max(var(--s2), env(safe-area-inset-top)); }
  .status[data-state="ready"] .status-number small { display: none; }
  .status[data-state="ready"] .status-words { gap: var(--s2); padding-block-end: max(var(--s2), env(safe-area-inset-bottom)); }
  .status[data-state="ready"] .status-title { padding-block: var(--s2); font-size: 2.75rem; }
  .status[data-state="ready"] .status-body { font-size: var(--step-0); }
  .status[data-state="ready"] .status-actions { margin-block-start: 0; }
}
/* Safari's tab bar on a phone on its side leaves the page 275 to 292px: the band, its word, the
   counter line and the gaps close up again, so Stop alarm stays on the screen above the home
   indicator. The band and that line hold one line each, so Arabic needs no room for a tail on the
   next. */
@media (max-height: 19.5rem) {
  .status[data-state="ready"] .pager-stage { padding-block-start: max(var(--s1), env(safe-area-inset-top)); }
  .status[data-state="ready"] .status-words { gap: var(--s1); }
  .status[data-state="ready"] .status-title { padding-block: var(--s1); font-size: 2.25rem; }
  .status[data-state="ready"] .status-title > :lang(ar) { line-height: 1.1; }
  .status[data-state="ready"] .status-body { line-height: 1.1; }
}
@keyframes lamp-breathe {
  50% { opacity: 0.85; }
}
@keyframes pager-hop {
  35% { transform: translateY(-2.5%); }
}
/* A lamp's soft edge: on fast, off fast, in step with the 1.2 s beep. */
@keyframes board-flip {
  50% { background-color: transparent; color: var(--fg); }
}
@keyframes flood {
  0%, 44% { opacity: 1; }
  50%, 94% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes lamp-blink {
  0%, 44% { opacity: 0.25; }
  50%, 94% { opacity: 1; }
  100% { opacity: 0.25; }
}
/* 2.4 s, the vibration's cycle (status.js buzz): three 0.4 s rattles 0.2 s apart, then still. */
@keyframes pager-buzz {
  0%, 16.6%, 25%, 41.6%, 50%, 66.6%, 100% { transform: none; }
  2%, 6%, 10%, 14%, 27%, 31%, 35%, 39%, 52%, 56%, 60%, 64% { transform: translateX(-3px) rotate(-2deg); }
  4%, 8%, 12%, 16%, 29%, 33%, 37%, 41%, 54%, 58%, 62%, 66% { transform: translateX(3px) rotate(2deg); }
}

/* 10. Staff sign-in: the PIN row sits at the top so the tablet keyboard never covers it */
/* On staff and owner pages the name takes the header's free room. A name left exactly as wide as
   its text can wrap in WebKit, though only the header's font-kerning rule stops it breaking a word. */
:is(.page:has(.staff-board), .page:has(.login), .admin) > .venue > h1 { flex-grow: 1; }
.login { display: grid; gap: var(--s3); }
.login > h2 { font-size: var(--step-3); font-weight: 800; line-height: 1.2; }
.login form { display: flex; flex-wrap: wrap; align-items: end; gap: var(--s3) var(--s4); }
.login .field { flex: 0 1 16rem; }
.login .field > label, .board-add .field > label { font-size: var(--step-1); }
/* The field stands as tall as the key and its edge, so their tops and bottoms line up */
:is(.login, .board-add) .input-number { block-size: calc(var(--tap-staff) + 0.25rem); }
:is(.login, .board-add) .btn { min-block-size: var(--tap-staff); }

/* 11. Staff board: warm paper, ink and the moulded keys. Aligned rows like a departures board,
   no boxes. Numbers are heavy tabular Rubik, as on the buzzer's face. */
.staff-board {
  --board-num: var(--step-4);
  display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: var(--s6); align-items: start;
}
.board-add { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: end; gap: var(--s3) var(--s4); }
.board-add .field { flex: 1 1 26.25rem; max-inline-size: 26.25rem; }   /* shows the 12 capitals of the longest number; a narrower row drops the key under it */
/* The Add error takes its own line, so the button stays level with the field */
.board-add > .notice { flex-basis: 100%; }
.board-notice { grid-column: 1 / -1; }
/* A landscape tablet: the board's notice ("Not saved", "Connection lost", "added again") sits in the
   Add row's empty end, level with the field and the key, so it takes no row of its own and the lists
   never move under a finger. Every tap clears it, so on its own row it moved the rows twice, and a
   quick second tap on Ready could land on the next row. The Add form's own error keeps its line under
   the field and the key, no wider than they are, so the two never overlap. */
@media (min-width: 60rem) {
  .board-add { grid-row: 1; display: grid; grid-template-columns: minmax(0, 26.25rem) auto minmax(0, 1fr); }
  .board-add > .notice { grid-column: 1 / 3; inline-size: 0; min-inline-size: 100%; }   /* its words never widen the key's column */
  .board-notice {
    grid-row: 1; justify-self: end; align-self: end; display: flex; align-items: center;
    inline-size: calc(100% - 26.25rem - 9rem); min-block-size: calc(var(--tap-staff) + 0.25rem);
  }
}
.board-col > h2 {
  display: flex; align-items: baseline; gap: var(--s3); margin-block-end: var(--s2);
  font-size: var(--step-2); font-weight: 800; line-height: 1.2;
}
.board-col > h2 > span:last-child { color: var(--fg-2); font-weight: 600; font-variant-numeric: tabular-nums lining-nums; }
.board-col > p { color: var(--fg-2); }
.board-list { display: grid; column-gap: var(--s4); }
/* Rows differ in height (Ring again under Collected, Washing under Ready on a car not in the wash
   yet), so each sits on its top edge: numbers, status lines and keys stay level across rows and
   across the two columns. */
.board-row {
  display: grid; grid-template-columns: 7.25rem minmax(0, 1fr) auto;
  align-items: start; column-gap: var(--s4); padding-block: var(--s2);
}
/* The number's column grows to six characters and a longer number wraps. On an upright tablet
   two lists share the width and six would push the status under the row's key, so there the
   lists take equal shares, the number comes down a step and its column stops at four. The status
   says whether to call the number out, so it wins: a number of five or more wraps. Below that
   width the list has the room, or the keys drop under it. */
@supports (grid-template-columns: subgrid) {
  .board-list { --rem-up: 1; grid-template-columns: fit-content(calc(10.75rem / var(--rem-up))) minmax(0, 1fr) auto; }
  html:lang(ar) .board-list { --rem-up: var(--ar-scale); }   /* a rem is larger there, the number not */
  .board-row { grid-column: 1 / -1; grid-template-columns: subgrid; }
  @media (min-width: 48rem) and (max-width: 59.99rem) {
    .board-list { column-gap: var(--s3); grid-template-columns: fit-content(5.75rem) minmax(0, 1fr) auto; }
    .board-row { column-gap: var(--s3); }
  }
  @media (min-width: 60rem) {   /* eight characters of a plate on one line, at the car wash's smaller number */
    .staff-board[data-car-wash] .board-list { grid-template-columns: fit-content(calc(11.5rem / var(--rem-up))) minmax(0, 1fr) auto; }
  }
}
/* A landscape tablet: this gap and the board's 1.2 to 1 split keep Collected beside the status
   with a five-digit number at 1024 wide. That takes 193px in English: Phone linked 102, the gap
   12, Collected 79. Six digits send Collected under the status. */
@media (min-width: 60rem) {
  .board-list, .board-row { column-gap: var(--s3); }
  /* A plate is what staff read aloud, so it stays whole: at 30px eight Hs, or four Ws and four digits, fit
     the 184px column. At 33px they take 201px, and a 204px column sends the status to three or four lines.
     The smaller key keeps the Arabic status on two lines at 1024 wide. */
  .staff-board[data-car-wash] { --board-num: 1.875rem; }
  .staff-board[data-car-wash] .board-row .btn { min-inline-size: 7rem; }
}
/* The number is also the Show QR button: a tap brings back that order's QR code. It adds no
   column, so the Ready column still fits a 1024px tablet. "Show QR" sits under it like the
   time sits under the status in the next cell. Its form fills the row's height, and so does
   the button, so the tap target is the whole cell. */
.board-row-qr { display: grid; align-self: stretch; }
.board-row-number {
  display: grid; justify-items: start; align-content: start; gap: var(--s1); min-block-size: var(--tap-staff);
  margin: 0; padding: 0; border: 0; background: none; color: var(--fg);
  font: 800 var(--board-num)/1 var(--font); font-variant-numeric: tabular-nums lining-nums;
  text-align: start; cursor: pointer; touch-action: manipulation;
}
.board-row-number > bdi { overflow-wrap: anywhere; }
/* A number is Latin, so an Arabic page's larger type leaves it alone and the status keeps its room. */
html:lang(ar) .board-row-number > bdi { font-size: calc(var(--board-num) / var(--ar-scale)); text-align: right; }   /* dir="ltr" would send a wrapped plate to the far side */
.board-row-number small {
  color: var(--fg-2); font: 600 var(--step-0)/1.3 var(--font); white-space: nowrap;
  text-decoration: underline; text-underline-offset: 0.2em;
}
/* The status is read, the time is secondary (departures board, Hierarchy): the word in ink, the time
   in ink-2. Every row's word is ink whichever word it is, so no row changes colour. */
.board-row-meta { color: var(--fg-2); line-height: 1.35; }
.board-row-word { color: var(--fg); }
/* Ready rows: Collected, and under it Ring again as quiet underlined text (the style of Show
   QR), so the Ready column gains no fourth column. Its tap target is 56px tall and 12px below
   Collected, because a slip onto Collected cannot be undone. Preparing rows: Collected the
   same quiet way at the end of the status cell, level with Ready and not between two rows'
   Ready buttons, so a number of up to five digits keeps the row's height; staff.js asks for a
   second tap. A car wash's car not in the wash yet (M4): under Ready, Washing the same quiet
   way, one tap (it is silent and only moves the car on). */
.board-row-actions { display: grid; justify-items: stretch; gap: var(--s3); }
.board-row-mid {
  display: flex; flex-wrap: wrap; align-items: start; justify-content: space-between;
  column-gap: var(--s3); min-inline-size: 0;
}
.board-row-ring, .board-row-quiet, .board-row-wash { display: grid; }
/* Collected and "Tap again" share one cell, so the button is as wide as the longer label in
   both states and the row does not move between the two taps. */
/* A set width, so its label wraps before the status does; on a narrow board it drops under. */
.board-row-mid > .board-row-quiet { flex: 0 1 4.5rem; }
/* Arabic's label takes one line at 5rem, where the row has it. Its set width is lower, 70px, because the
   time's digits are proportional: the status runs up to 101px, and at 4.5rem a five-digit row sends the
   control under it at some minutes of the day. With that number the label takes two lines and the row
   keeps its height. A wider set width or nowrap (the hidden Tap again is wider still) would send the
   control under the status. */
html:lang(ar) .board-row-mid > .board-row-quiet { flex: 1 1 4.125rem; max-inline-size: 5rem; }
/* A car wash: Collected always under the status, at the start, so it is in one place on every row in
   both languages (section 5, Car washes). A car in the wash's row grows to about 116px. */
.staff-board[data-car-wash] .board-row-mid > .board-row-quiet { flex: 1 0 100%; max-inline-size: none; }
.staff-board[data-car-wash] .board-row-mid > .board-row-quiet > button { justify-self: start; }
.board-row-quiet > button { display: grid; place-content: center; }
.board-row-quiet > button > span { grid-area: 1 / 1; }
.board-row-quiet > button > span + span,
.board-row-quiet[data-armed] > button > span:first-child { visibility: hidden; }
.board-row-quiet[data-armed] > button > span + span { visibility: visible; }
.board-row-ring > button, .board-row-quiet > button, .board-row-wash > button {
  min-block-size: var(--tap); margin: 0; padding: 0; border: 0; background: none;
  color: var(--fg-2); font: 600 var(--step-0)/1.3 var(--font);
  text-decoration: underline; text-underline-offset: 0.2em; cursor: pointer; touch-action: manipulation;
}
.btn-ready, .btn-collected { min-block-size: var(--tap-staff); min-inline-size: 9.5rem; }
/* Plates run longer than receipts, so a car wash shares the width evenly. */
@media (min-width: 48rem) {
  .staff-board[data-car-wash] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 48rem) and (max-width: 59.99rem) {
  .staff-board { --board-num: var(--step-3); grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .staff-board[data-car-wash] { --board-num: var(--step-2); }   /* a long plate takes two lines, not three */
}
@media (max-width: 63.99rem) {   /* an upright tablet, or a 10-inch one 960 wide: the keys leave the status room */
  .btn-ready, .btn-collected { min-inline-size: 7rem; padding-inline: var(--s3); }
}
@media (max-width: 47.99rem) {
  .staff-board { grid-template-columns: minmax(0, 1fr); }
}
/* A bar phone has no room for three cells: number and status share a line and the row's keys
   run the full width under them, with Ring again or Washing still 24px clear of the key. */
@media (max-width: 29.99rem) {
  .page:has(.staff-board) { --gutter: 1rem; }
  .page:has(.staff-board) .venue > .btn { padding-inline: var(--s3); white-space: nowrap; }
  /* The header takes a second row only when it needs one, and whatever drops keeps to the end
     edge. The name runs no wider than the room beside the widest logo (9rem) or the small pager,
     so it stays by its logo, and a long name sends both keys down together. Child selectors:
     the QR overlay has a .venue of its own, and wrapping that one would push Close off a phone. */
  .page:has(.staff-board) > .venue { flex-wrap: wrap; justify-content: flex-end; row-gap: var(--s2); }
  .page:has(.staff-board) > .venue > h1 { margin-inline-end: auto; max-inline-size: calc(100% - 9rem - var(--s3)); overflow-wrap: anywhere; }
  .page:has(.staff-board) > .venue:not(:has(img)) > h1 { max-inline-size: calc(100% - 1.75rem - var(--s3)); }
  /* The Add field takes the row, and its type gives way just enough to show 12 capitals (9.3 em) */
  .board-add .field { flex-basis: 100%; max-inline-size: none; }
  .board-add .input-number { font-size: min(var(--step-4), (100vw - 2 * var(--gutter) - 2 * var(--s4) - 4px) / 9.35); }
  .board-row { row-gap: var(--s2); }
  .board-row > :last-child { grid-column: 1 / -1; }
  .btn-ready, .btn-collected { inline-size: 100%; }
  .board-row-actions { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--s5); }
}

/* 12. Motion: reduced motion stops all of it */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}

/* 13. Builder additions (M1). [hidden] must beat the display that components set, because
   status.js switches state by toggling hidden on each state's title, body and actions. .lead
   is the message on a not found page. The Ready title is the single word --ready-word was
   sized for ("Ready" / "جاهز"); "Please come to the counter." is the Ready body. */
[hidden] { display: none !important; }
.lead { font-size: var(--step-2); font-weight: 600; line-height: 1.3; }
/* Overrides section 6: a pair span keeps the page direction for its box (so the margin that
   pushes the second script to its edge still works), but each paragraph inside takes its
   direction from its own text. With `isolate`, an English title that wrapped on an Arabic page
   was laid out right to left and read ".to the counter". */
:is(.status-title, .pair) > [lang] { unicode-bidi: plaintext; }

/* 14. QR overlay on the staff tablet. It fills the browser window over the board, turned to
   face the customer. The QR code is the one element that ignores the theme: black on white
   with its quiet zone (app.qr_svg), because phone scanners need dark modules on a light
   ground. The venue's colour rims it outside the quiet zone, so nothing touches the code's own
   margin, and it holds still: a code that moves scans worse. Landscape: the code on the start
   side, the venue then the text centred beside it. Portrait: venue, code, text, top to bottom
   (place first). --w is the width of the code with its rim. In landscape it takes the height
   but leaves the text column at least 22rem (6rem is the gutters and the column gap), so the
   scan line and Close still fit on an 800px-wide tablet. --qr-max caps it on every screen: a
   480px code (9.2 cm on an iPad 10.9", 9.6 cm on a 13" laptop) fits a phone's frame from about
   15 cm and still reads at 60 cm; at the full height of a landscape tablet (13.6 cm) a customer
   had to step back to scan (the founder, 9 Nov 2026). It is in px, not rem, because it is a size
   on the glass: an Arabic board's rem is 1.0625 larger and would get a larger code. */
.qr-overlay {
  --qr-max: 504px;
  --vh: 100vh; --w: min(var(--vh) - 3rem, 100vw - 28rem, var(--qr-max));
  position: fixed; inset: 0; inline-size: 100%; block-size: 100%;
  max-inline-size: none; max-block-size: none; margin: 0; padding: var(--s5) var(--gutter);
  border: 0; background: var(--bg); color: var(--fg); overflow: auto;
}
@supports (height: 100dvh) {
  .qr-overlay { --vh: 100dvh; }
}
.qr-overlay[open] {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: 1fr auto auto 1fr; grid-template-areas: "code ." "code venue" "code text" "code .";
  align-items: center; column-gap: var(--s7);
}
.qr-code {
  --rim: 0.75rem;
  grid-area: code; display: block; inline-size: calc(var(--w) - 2 * var(--rim)); block-size: calc(var(--w) - 2 * var(--rim));
  margin: var(--rim); border-radius: var(--rim); background: #FFFFFF; box-shadow: 0 0 0 var(--rim) var(--puck);
}
.qr-overlay:focus { outline: none; }   /* focused as a whole on open (staff.js); Close keeps its ring */
/* Scanned: the code goes and its place stays, so the words beside it do not move. */
.qr-overlay[data-state="linked"] .qr-code { visibility: hidden; }
.qr-overlay > .venue {
  grid-area: venue; min-block-size: auto; margin-block-end: var(--s4); padding-block: 0;
  font-size: var(--step-2);
}
/* Any name the app takes stays on two lines, with no piece of a third. Arabic ink runs from 1 em
   above the baseline (أ) to 0.48 em below it (the end of ي, ع, ج), so a clip can only pass
   between one line's tails and the next line's tops with the lines 1.6 apart. The line box alone
   would cut those tails: the clip's small margin keeps them whole. */
.qr-overlay > .venue p {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  line-height: 1.6; padding-block: 0.09em; margin-block: -0.09em;
}
.qr-text { grid-area: text; display: grid; gap: var(--s4); align-content: center; container-type: inline-size; }
.qr-caption { color: var(--fg-2); font-size: var(--step-1); font-weight: 600; line-height: 1.2; }
/* staff.js counts characters, so the fit allows 0.74 em for each, a capital's width: digits and
   plates stay on one line, and a run of M and W breaks rather than widen the overlay. */
.qr-number {
  font: 800 clamp(3.5rem, 12vw, var(--num-max, 9rem))/1 var(--font);
  font-variant-numeric: tabular-nums lining-nums; overflow-wrap: anywhere;
}
@supports (width: 1cqi) {
  .qr-number { font-size: min(var(--num-max, 9rem), calc(100cqi / (0.74 * var(--chars, 4)))); }
}
.qr-say { font-size: var(--step-3); font-weight: 800; line-height: 1.2; }
.qr-say > [lang] { text-wrap: balance; }   /* a wrapped scan line breaks evenly, no lone word */
/* Close and Show orders are for staff, so they are quiet text at the end of the column (like
   "Show QR" on the board), not filled buttons under the customer's instruction. 44px tap
   target. Each works only when held (staff.js, audit row 17): no long-press menu, no text
   selection, no scrolling from it, and while it is held its text turns to ink, a change of
   colour and nothing that moves. */
.qr-staff { display: grid; justify-items: end; gap: var(--s2); }   /* stacked at the end edge */
.qr-note { color: var(--fg-2); }   /* what Replace phone did, in the staff's own corner */
.qr-hold {
  min-block-size: 2.75rem; min-inline-size: 2.75rem; margin: 0;
  padding: 0 var(--s2); border: 0; background: none; color: var(--fg-2);
  font: 600 var(--step-0)/1.3 var(--font); text-decoration: underline;
  text-underline-offset: 0.2em; cursor: pointer; touch-action: none;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
.qr-hold[data-held] { color: var(--fg); }
/* Covered: the code and the number are gone, so the venue and Show orders sit in one column.
   The staff board under it is hidden from the customer until staff hold Show orders. */
.qr-overlay[open][data-state="covered"] {
  grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr auto auto 1fr;
  grid-template-areas: "." "venue" "text" ".";
}
/* A staff tablet is not pulled down to reload while the customer holds it. After a reload
   while covered, cover.js hides the board and its Sign out until staff.js covers it again. */
html:has(.staff-board) { overscroll-behavior-y: none; }
html[data-covered] :is(.staff-board, [form="signout"]) { display: none; }   /* no child can show again */
/* Where two of those tall lines would not fit, the name takes one line that does not wrap: with
   no next line to show, it keeps the header's tight 1.2 and a clip wide enough for any mark. */
@media (max-height: 40.5rem), (orientation: portrait) and (max-height: 61.5rem),
       (orientation: portrait) and (max-width: 37.49rem) {
  .qr-overlay > .venue p {
    display: block; white-space: nowrap; text-overflow: ellipsis;
    line-height: 1.2; padding-block: 0.35em; margin-block: -0.35em;
  }
}
/* A 1024 by 600 tablet or a bar phone. Everything fits with no scrolling, on an Arabic board and
   with a logo too. Only a failed Replace phone's note can still scroll the overlay, by 38px at
   worst, with Close at most 22px past the fold. */
@media (max-height: 38rem), (orientation: portrait) and (max-width: 37.49rem) {
  .qr-text { --num-max: 7.75rem; gap: var(--s3); }
  .qr-overlay > .venue { margin-block-end: var(--s3); }
  .qr-overlay > .venue img { block-size: 2.25rem; }
}
@media (max-height: 35rem) {   /* a short landscape screen, down to 800 by 500 */
  .qr-text { --num-max: 6.5rem; }
  .qr-say { font-size: var(--step-2); }
}
@media (orientation: portrait) {
  /* 40% of the visible height, less where that would not leave the 33rem the text and Close need,
     but never under 12rem, which still scans: in a near-square split view the overlay scrolls. */
  .qr-overlay { --w: min(100vw - 2 * var(--gutter), var(--vh) * 0.4, max(12rem, var(--vh) - 33rem), var(--qr-max)); }
  .qr-overlay[open] {
    grid-template-columns: minmax(0, 1fr); grid-template-rows: none;
    grid-template-areas: "venue" "code" "text"; align-content: start; justify-items: center; row-gap: var(--s5);
  }
  .qr-overlay > .venue { margin-block-end: 0; justify-self: start; max-inline-size: 100%; }
  .qr-text { inline-size: 100%; }
}
@media (orientation: portrait) and (max-width: 37.49rem) {   /* narrower than any upright tablet: a bar phone, down to 360 by 704 */
  .qr-overlay { --w: min(100vw - 2 * var(--gutter), var(--vh) * 0.4, var(--qr-max)); padding-block: var(--s3); }
  .qr-overlay[open] { row-gap: var(--s3); }
  .qr-text { --num-max: 5rem; }
  .qr-say { font-size: var(--step-1); }
}
