/* The QR page, dressed as the customer app.
   ============================================================================

   This page used to wear the *vendor* app's palette — the warm chalkboard of
   `apps/vendor_app/lib/theme.dart`, with turmeric and tandoor teal — and said
   so in this comment. That was wrong about who is reading it. A vendor sees
   the chalkboard because they are working behind a counter all evening; the
   person who scans this code is a customer standing in front of one, and two
   minutes later they may open the customer app and see the same stall. Those
   two surfaces answering the same question in two different visual languages
   is the seam, not the consistency.

   So the tokens below are the customer app's counter palette
   (`apps/customer_app/lib/theme.dart`, BoboQCounter): a dark neutral board,
   one lit green reserved for live state, and white turned down twice for
   everything read rather than glanced at. Keep them in step with that file.

   **On the display face.** The app sets every figure in Exo 2 italic — the
   rule there is "the face is the app's own voice, it is never its labelling".
   This page does not load it. A webfont is a blocking download on exactly the
   weak connection the QR page spec exists to be fast on, and it would be the
   only asset request this page makes. Figures here are set italic, heavy and
   tabular in the system face instead: the same gesture, none of the bytes.
   That is a deliberate approximation, not an oversight. */

:root {
  /* The board's unlit ground, and the raised surfaces on it. */
  --ground: #212121;
  --board: #2e2e2e;

  /* Lit. Live state, and nothing else — an open stall, a ready token, the
     control that takes your place in the line. Green that covers a screen
     stops meaning live. */
  --lit: #5dd62c;
  --unlit: #337418;

  --text: #f8f8f8;
  /* Small print: the same white, turned down. The palette has no mid grey,
     and inventing one would put another colour on the screen. */
  --quiet: rgba(248, 248, 248, 0.55);
  --fainter: rgba(248, 248, 248, 0.45);
  --edge: rgba(248, 248, 248, 0.14);
  --rim: rgba(248, 248, 248, 0.24);

  /* The one alarming colour, for a refusal. Lifted off a print red, which is
     unreadable on a near-black screen. */
  --danger: #ff6b5a;

  /* The FSSAI diet marks, carried over unchanged: these are recognised
     before they are read, so they are not ours to restyle. */
  --veg: #0a8020;
  --egg: #e9a400;
  --non-veg: #d93a1f;

  /* One place for the prep tile's size, because the dashed rule between
     dishes has to start where the tile ends. */
  --tile: 3.375rem;
  --tile-gap: 0.875rem;
}

[hidden] {
  display: none !important;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--text);
  background: var(--ground);
  min-height: 100vh;
  line-height: 1.4;
}

#app {
  max-width: 480px;
  margin: 0 auto;
  padding: 1.25rem 1rem 3rem;
  /* Tall enough to reach the bottom of the window even when a stall sells
     three dishes, so the footer below can be pushed down to meet it rather
     than floating in the middle of an empty screen. box-sizing accounts for
     the padding above, which would otherwise push the page into a scroll
     that has nothing in it. */
  box-sizing: border-box;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* The one view long enough to need this. It stretches to whatever #app has,
   so `margin-top: auto` on the footer inside it has room to work. */
#vendor-view {
  flex: 1;
  display: flex;
  flex-direction: column;
}

a,
button,
input,
[tabindex] {
  outline-offset: 2px;
}
a:focus-visible,
button:focus-visible,
input:focus-visible {
  outline: 2px solid var(--lit);
}

/* --- Stall header ------------------------------------------------------ */

.stall-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}

.stall-header h1 {
  font-size: 1.4rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  /* Set as a name, not as a label. It was uppercased, which on a stall called
     "SEC-18 Chaap" turned a proper noun into signage — the app prints the
     vendor's own capitalisation and so does this. */
}

.pill {
  display: inline-block;
  padding: 0.25rem 0.65rem;
  border-radius: 999px;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
}
.pill--open {
  color: var(--lit);
  border: 1px solid var(--lit);
}
.pill--paused {
  color: var(--danger);
  border: 1px solid var(--danger);
}
.pill--pending {
  color: var(--quiet);
  border: 1px solid var(--quiet);
}
.pill--ok {
  color: var(--lit);
  border: 1px solid var(--lit);
}
.pill--blocked {
  color: var(--danger);
  border: 1px solid var(--danger);
}

/* Whether the stall is taking anyone right now.
   ---------------------------------------------------------------------------
   A dot and a word, where an outlined OPEN pill used to be. The pill read as a
   button — a bordered capsule beside a heading is a control everywhere else on
   a phone — and it shouted a state that is true most of the time. The dot is
   the live signal and carries the colour; the word just says what the dot
   means, in the same turned-down white as the rest of the reading matter.

   "Serving" rather than "Open", because the question a customer is actually
   asking at a stall is whether food is coming out of it. */
.serving {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--quiet);
  white-space: nowrap;
}
.serving::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--lit);
}
/* A stall that has paused. The dot loses the lit colour rather than gaining a
   new one: the palette's green means live, and there is no "live, but off". */
.serving--paused::before {
  background: var(--fainter);
}

/* Not at the stall.
   ---------------------------------------------------------------------------
   The whole page, when the location check says the phone is somewhere else. */
#too-far {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.25rem;
  padding: 2.5rem 0;
}
#too-far p {
  font-size: 1.05rem;
  line-height: 1.5;
  color: var(--text);
}
#too-far button {
  appearance: none;
  background: none;
  border: 1.4px solid var(--unlit);
  border-radius: 12px;
  padding: 0.8rem 1.4rem;
  color: var(--lit);
  font-size: 0.9rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: pointer;
}

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

#menu {
  margin-bottom: 1.5rem;
}

/* One dish.
   ---------------------------------------------------------------------------
   Three columns, the same reading order the app's menu screen uses: how long
   it takes, what it is and what it costs, and the control that adds it. The
   prep time leads because it is the number the choice is actually made on
   while somebody is standing at the stall deciding whether to wait. */
.menu-item {
  position: relative;
  display: grid;
  grid-template-columns: var(--tile) 1fr auto;
  align-items: center;
  gap: var(--tile-gap);
  /* 0.85rem once, so 1.7rem between any two rows. Tightened on 21 September
     2026 at the owner's request, in the same pass that removed the rule
     between dishes — the two changes pull against each other and the pair had
     to be looked at together. Less air and no divider is the point at which
     rows start reading as one block, and the prep tile is what stops it: each
     row opens with a raised square, so the eye has a left edge to count down
     even with the gaps closed. */
  padding: 0.5rem 0;
}

/* No rule between dishes. There was a perforated one, starting where the
   dish's name starts, and it was removed on 21 September 2026 at the owner's
   request: the rows already separate themselves. Each one is a raised prep
   tile, a name, a price and a control, with its own vertical rhythm, and a
   line through every gap was a second answer to a question the layout had
   already answered.

   The app's menu still draws one. That is a real divergence rather than an
   oversight — the app lists a stall's whole menu for reading, this page lists
   it for ordering from at a counter, and fewer marks is the right call on the
   surface somebody is using while standing up. If the app's menu is ever
   restyled, this note is the reason the two differ. */

/* The prep tile: a raised square holding one figure. */
.menu-item__prep {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: var(--tile);
  height: var(--tile);
  border-radius: 12px;
  background: var(--board);
  border: 1px solid var(--edge);
}
.menu-item__minutes {
  /* A figure, so it takes the display gesture: italic, heavy, tabular. */
  font-style: italic;
  font-weight: 800;
  font-size: 1.25rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.menu-item__min {
  font-size: 0.5rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--fainter);
  margin-top: 1px;
}

/* The diet mark: a rule under the tile rather than a badge beside the name.
   Short of the tile's width and centred, so it reads as belonging to the tile
   and takes no width from the dish. */
.menu-item__diet {
  width: 70%;
  height: 4px;
  border-radius: 2px;
  margin: 5px auto 0;
}
.menu-item__diet--veg { background: var(--veg); }
.menu-item__diet--egg { background: var(--egg); }
.menu-item__diet--non_veg { background: var(--non-veg); }

.menu-item__body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}
.menu-item__name {
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.25;
}
.menu-item__price {
  /* Under the name rather than in a column of its own. The prices used to be
     centred in a middle column, which left them neither aligned with each
     other nor attached to the dish they belonged to — ₹150, ₹20 and ₹120 each
     sat in a different place. A price belongs to its dish. */
  font-style: italic;
  font-weight: 700;
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}

.menu-item--sold-out .menu-item__name,
.menu-item--sold-out .menu-item__price {
  color: var(--fainter);
  text-decoration: line-through;
}
.menu-item--sold-out .menu-item__prep {
  background: var(--ground);
}
.menu-item--sold-out .menu-item__minutes {
  color: var(--fainter);
}

/* The stepper.
   ---------------------------------------------------------------------------
   Two glyphs and a figure, with no button chrome at all. They were rendering
   as the browser's default buttons — grey boxes with a hairline — because
   nothing in this file had ever styled them: three small slabs in the corner
   of every row, competing with the dish for attention and looking like a
   rendering fault.

   Green because adding a dish is what this page is for; that is the one thing
   on the board the lit colour is licensed for. */
.stepper {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.stepper__button {
  appearance: none;
  background: none;
  border: 0;
  padding: 0 0.25rem;
  /* A comfortable target on a phone held in one hand at a stall, without
     drawing a box around it. */
  min-width: 2rem;
  height: 2.25rem;
  font-size: 1.5rem;
  line-height: 1;
  font-weight: 700;
  color: var(--lit);
  cursor: pointer;
}

/* ADD, before anything has been added.
   ---------------------------------------------------------------------------
   Outlined in the green turned down, exactly as the customer app draws it:
   what separates ADD from the control that commits the order is fill, not
   colour. Nothing on this list is a filled lit surface, so the pay button at
   the foot of the page still outranks every row above it. */
.stepper__add {
  appearance: none;
  background: none;
  height: 2.375rem;
  padding: 0 1rem;
  border: 1.4px solid var(--unlit);
  border-radius: 10px;
  color: var(--lit);
  font-size: 0.9rem;
  font-weight: 800;
  letter-spacing: 0.05em;
  cursor: pointer;
}
.stepper__count {
  min-width: 1ch;
  text-align: center;
  font-style: italic;
  font-weight: 800;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  color: var(--fainter);
}
.stepper--active .stepper__count {
  color: var(--text);
}

/* --- Join form ----------------------------------------------------------- */

#join-form {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

#join-form label {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.68rem;
  font-weight: 800;
  color: var(--fainter);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

/* Why a field is being asked for, under the field.
   Inside the <label>, so it inherits that rule's uppercase, 800 weight and
   letter-spacing — all three are wrong for a sentence and all three are undone
   here. The label is a tag and this is prose; they should not look alike. */
.field-hint {
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--quiet);
  text-transform: none;
  letter-spacing: normal;
  line-height: 1.4;
  margin-top: -0.1rem;
}

/* The one hint that has to be read, not skimmed: the number is typed in front
   of the vendor, and "the vendor keeps it" is the fear that stops people
   typing it. Full-strength text and weight with the lit rail beside it, so it
   reads as a promise rather than small print — still undoing the label's
   uppercase and tracking, because it is a sentence. */
.field-assurance {
  display: block;
  font-size: 0.875rem;
  font-weight: 800;
  color: var(--text);
  text-transform: none;
  letter-spacing: normal;
  line-height: 1.35;
  padding: 0.45rem 0 0.45rem 0.7rem;
  border-left: 3px solid var(--lit);
}

#join-form input {
  font-size: 1rem;
  padding: 0.85rem 0.9rem;
  /* Sunk into the board rather than drawn on it, matching the app's fields:
     a filled well reads as somewhere to type, an outline reads as a label. */
  background: var(--ground);
  border: 1px solid var(--edge);
  border-radius: 12px;
  color: var(--text);
}
#join-form input::placeholder {
  color: var(--fainter);
}
#join-form input:focus {
  outline: none;
  border-color: var(--lit);
}

/* The two ways into the line.
   ---------------------------------------------------------------------------
   Paying is the one the app is arranged around, so it is the filled, lit
   control. Joining free is real and stays available, but it is drawn as the
   quieter of the two rather than as a second shout. */
#join-form button {
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 1rem;
  border-radius: 14px;
  border: 1px solid transparent;
  background: var(--lit);
  /* Dark text on the lit green, which is the only combination on this palette
     that clears contrast at this size. */
  color: var(--ground);
  cursor: pointer;
}
#join-form button.button--secondary {
  background: transparent;
  border-color: var(--unlit);
  color: var(--lit);
}
#join-form button:disabled {
  /* Flattened into the board rather than greyed with a new colour. A control
     that cannot be pressed should recede, not announce itself. */
  background: var(--board);
  border-color: transparent;
  color: var(--fainter);
  cursor: not-allowed;
}
#join-form button.button--secondary:disabled {
  background: transparent;
  border-color: var(--edge);
  color: var(--fainter);
}

.error {
  color: var(--danger);
  font-size: 0.85rem;
}

/* --- Post-join status view: the ticket -----------------------------------

   A copy of the customer app's queue screen — `LiveQueueScreen` in
   apps/customer_app/lib/screens/live_queue_screen.dart. The owner's call, 27
   September 2026: a customer holding a link and a customer holding the app
   are holding the same ticket. The values below are the app's own, converted
   to CSS: its dark board and scanlines, the giant green count in its display
   face, the green card at radius 24, the spaced capitals, the stub pinned to
   the foot of the screen with its tear and two bites.

   The display face is Exo 2 italic, as in the app, subset to the characters a
   ticket prints — 24 KB against the full file's 302 — because this page's
   brief is load time on a weak connection. Its OFL licence travels beside it
   in fonts/. If it has not arrived yet the ticket falls back to the system's
   own heavy italic, which reads as the same kind of number.
   ------------------------------------------------------------------------- */

@font-face {
  font-family: "Exo 2 Ticket";
  src: url("../fonts/exo2-italic-subset.woff2") format("woff2");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

#status-view {
  --display: "Exo 2 Ticket", -apple-system, "Segoe UI", Roboto, sans-serif;
  --pending: #ffc107;
  --card-ink: #212121;
}

/* The whole screen, as the app's is: the stall along the top, the ticket
   scrolling under it, and the stub fixed to the bottom. Fixed over #app rather
   than restyling `body`, so the menu underneath is untouched. `[hidden]` still
   wins — the global rule carries `!important`. */
#status-view:not([hidden]) {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  /* The board: the scanline grain (`BoardScanlines` — black at 13%, one line
     every three points), a green light behind the number, the dark ground. */
  background:
    repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0.13) 0 1px, transparent 1px 3px),
    radial-gradient(ellipse 70% 38% at 50% 24%, rgba(93, 214, 44, 0.12), transparent 70%),
    var(--ground);
}

/* The stall and whether it is serving — the app's `_HeaderRail`. */
.ticket-rail {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  padding: 1.5rem 1.5rem 0.5rem;
}
.ticket-rail h1 {
  font-size: 1.45rem;
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--text);
}
/* SERVING with its green dot: words in spaced capitals, not a bordered pill. */
.ticket-rail .pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
  margin-top: 0.4rem;
  padding: 0;
  border: 0;
  background: none;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.2em;
}
.ticket-rail .pill::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: currentColor;
}
.ticket-rail .pill--open {
  color: var(--lit);
}
.ticket-rail .pill--paused {
  color: var(--quiet);
}

.ticket-body {
  flex: 1;
  overflow-y: auto;
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  padding: 0 1.5rem 1.75rem;
}

.token-hero {
  align-self: stretch;
  width: 100%;
  margin: 1.5rem 0 1rem;
}
/* The track holds the ticket and the promo side by side, so the hero is as
   tall as the taller of the two and nothing below it moves when they swap.
   Clipped only while rotating: the turn and confirming cards cast a shadow
   well past their edges, and neither of them ever rotates. Padding above and
   below only, giving the figure's glow room inside the clip: a clip includes
   its own padding, so side padding would show the next slide through it. */
.token-hero__track {
  display: flex;
  transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.token-hero__slide {
  flex: 0 0 100%;
  min-width: 0;
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  align-items: center;
}
.token-hero--rotates {
  overflow: hidden;
  margin-block: 0.5rem;
  padding-block: 1rem;
}
.token-hero[data-side="promo"] .token-hero__track {
  transform: translateX(-100%);
}
@media (prefers-reduced-motion: reduce) {
  .token-hero__track {
    transition: none;
  }
}

/* The app promo — the ticket's second face. Set on the stub's board colour
   so it reads as part of the ticket rather than an advert pasted over it,
   and absent entirely unless the hero is rotating. */
.app-promo {
  display: none;
}
.token-hero--rotates .app-promo {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1.5rem 1.25rem;
  border-radius: 24px;
  background: var(--board);
  border: 1px solid rgba(255, 255, 255, 0.07);
  text-align: center;
}
.app-promo__eyebrow {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--lit);
}
.app-promo__line {
  margin: 0;
  font-size: 1rem;
  line-height: 1.35;
  color: rgba(248, 248, 248, 0.7);
}
.app-promo__line--lit {
  font-family: var(--display);
  font-style: italic;
  font-weight: 900;
  font-size: 1.45rem;
  line-height: 1.15;
  color: #f8f8f8;
}
.app-promo__badges {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.5rem;
}
.app-promo__badge {
  display: block;
  line-height: 0;
  border-radius: 8px;
}
.app-promo__badge img {
  display: block;
  height: 40px;
  width: auto;
}
/* Google ships its badge with transparent padding built in, roughly a sixth
   of its height on every side. Drawn taller and pulled in by that margin, its
   visible badge sits the same height as Apple's. The artwork itself is never
   cropped: both companies require it unaltered. */
.app-promo__badge--play img {
  height: 58px;
  margin: -9px -9px;
}
.app-promo__badge:focus-visible {
  outline: 2px solid var(--lit);
  outline-offset: 3px;
}

.token-disc {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.token-disc--none {
  display: none;
}

.token-eyebrow,
.token-footer {
  display: none;
}

/* The figure — people ahead, in the display face, lit, and large enough to
   read from across a street. The app sets it at 194 points; this scales with
   the screen and stops at the same size. */
.token-disc--figure #token-number {
  font-family: var(--display);
  font-style: italic;
  font-weight: 900;
  font-size: clamp(8rem, 46vw, 12rem);
  line-height: 0.95;
  letter-spacing: -0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--lit);
  text-shadow: 0 0 44px rgba(93, 214, 44, 0.35);
}

/* The card — the app's `_TurnCard`: lit on the customer's turn, amber while
   the stall is asked. Printed in the dark ground's own colour, fixed, because
   the card is a flat colour whatever the page does. */
.token-disc--turn,
.token-disc--served,
.token-disc--confirming {
  width: 100%;
  padding: 1.75rem 1.5rem 1.4rem;
  border-radius: 24px;
  background: var(--lit);
  color: var(--card-ink);
  box-shadow:
    0 16px 70px rgba(93, 214, 44, 0.3),
    0 26px 34px rgba(0, 0, 0, 0.55);
}
.token-disc--confirming {
  background: var(--pending);
  box-shadow:
    0 16px 70px rgba(255, 193, 7, 0.25),
    0 26px 34px rgba(0, 0, 0, 0.55);
}
.token-disc--turn .token-eyebrow,
.token-disc--turn .token-footer,
.token-disc--served .token-eyebrow,
.token-disc--served .token-footer,
.token-disc--confirming .token-eyebrow,
.token-disc--confirming .token-footer {
  display: block;
  font-size: 0.85rem;
  font-weight: 800;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--card-ink);
}
.token-disc--turn #token-number,
.token-disc--served #token-number,
.token-disc--confirming #token-number {
  margin: 0.9rem 0 1rem;
  font-family: var(--display);
  font-style: italic;
  font-weight: 900;
  font-size: clamp(4rem, 22vw, 6rem);
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--card-ink);
}
/* The hash before the code, at the app's 40% black (0x66000000): part of the
   card's print, quieter than the four characters somebody reads out. */
.token-hash {
  color: rgba(0, 0, 0, 0.4);
}
/* SERVED on the green card, at exactly the green card's height. The turn
   card's two lines are laid out but invisible, and the word, a size smaller
   than the code because it is six letters where the code has four, sits in a
   line box the code's height with the code's margins. The card does not
   change size between IT'S YOUR TURN and SERVED at any width. */
.token-disc--served .token-eyebrow,
.token-disc--served .token-footer {
  display: block;
  visibility: hidden;
}
.token-disc--served #token-number {
  font-size: clamp(3.25rem, 17vw, 4.75rem);
  line-height: clamp(4rem, 22vw, 6rem);
}
/* The rule between the code and what to do with it. */
.token-disc--turn .token-footer,
.token-disc--served .token-footer,
.token-disc--confirming .token-footer {
  align-self: stretch;
  padding-top: 1rem;
  border-top: 1px solid rgba(33, 33, 33, 0.28);
  font-size: 0.8rem;
  letter-spacing: 0.26em;
}

/* SERVING IN · 14 MIN — the app's `_WaitLine`: the label turned down, the
   minutes full white, both in spaced capitals. A notice replaces both. */
.token-wait {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 0.85rem;
  margin: 0.25rem 0 0.5rem;
  text-align: center;
}
.token-wait:empty {
  display: none;
}
.token-wait__label {
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  color: var(--quiet);
}
.token-wait__value {
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  color: var(--text);
}
.token-wait__notice {
  font-size: 0.85rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  line-height: 1.5;
  text-transform: uppercase;
  color: var(--text);
}

.token-meta {
  text-align: center;
  color: var(--quiet);
  margin-bottom: 0.35rem;
}
.token-meta:empty {
  display: none;
}

/* What the stall has said — audit finding `H25`, step 4. Drawn only when
 * there is no readout to carry it (declined, or never confirmed), and then it
 * takes the readout's place; otherwise main.js keeps it for the screen reader
 * alone, since the wait line or the card already says the same thing.
 *
 * Deliberately not red. "Confirming your order…" is the ordinary state of
 * every paid order for its first ten seconds, and a stall that could not take
 * an order has not made the customer do anything wrong. */
.token-acceptance {
  margin: 2.5rem 0 1rem;
  text-align: center;
  font-size: 1.35rem;
  font-weight: 800;
  line-height: 1.3;
  color: var(--text);
}

/* A finished ticket's last word — served, refunded, cancelled. */
.token-final {
  margin-top: 1.5rem;
  text-align: center;
  font-size: 1.15rem;
  font-weight: 800;
  line-height: 1.35;
  color: var(--text);
}

/* The way to the stall, where the app has its outlined button. */
.token-button {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 3.5rem;
  margin-top: 1.75rem;
  border: 1.5px solid rgba(248, 248, 248, 0.32);
  border-radius: 14px;
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--quiet);
}
.token-button.is-spent {
  display: none;
}
.token-button:focus-visible {
  outline: 2px solid var(--lit);
  outline-offset: 3px;
}

/* The stub — the app's `_Counterfoil`, pinned below the scrolling ticket.
   Board-coloured and free of the grain: the scanlines are the board's, and a
   piece torn off the board does not carry them.

   The two bites are holes, not discs painted in the page colour: a mask cuts
   a quarter-circle out of each top corner, so whatever the board is wearing —
   the grain included — shows through them. A painted disc is the trick the
   app used, and it reads as a patch the moment the board behind it has a
   texture. */
.ticket-stub {
  position: relative;
  flex-shrink: 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  padding: 1.35rem max(1.5rem, calc((100% - 480px) / 2 + 1.5rem))
    calc(1.35rem + env(safe-area-inset-bottom));
  background: var(--board);
  -webkit-mask:
    radial-gradient(circle 13px at 0 0, transparent 12.5px, #000 13px),
    radial-gradient(circle 13px at 100% 0, transparent 12.5px, #000 13px);
  -webkit-mask-composite: source-in;
  mask:
    radial-gradient(circle 13px at 0 0, transparent 12.5px, #000 13px),
    radial-gradient(circle 13px at 100% 0, transparent 12.5px, #000 13px);
  mask-composite: intersect;
}
/* The tear, between the bites. */
.ticket-stub::before {
  content: "";
  position: absolute;
  top: 0;
  left: 1.1rem;
  right: 1.1rem;
  border-top: 1.5px dashed var(--edge);
}
.ticket-stub__label {
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--quiet);
}
/* Hidden on a finished ticket, not removed: the slot stays, so the code sits
   at the same height either way — the app's own rule. */
.ticket-stub__label.is-spent {
  visibility: hidden;
}
.ticket-stub__value {
  margin-top: 0.35rem;
  font-size: 2.35rem;
  font-weight: 900;
  line-height: 1.05;
  letter-spacing: 0.12em;
  color: var(--text);
}
.ticket-stub__hash {
  color: var(--quiet);
}
.ticket-stub__issued {
  text-align: right;
}
.ticket-stub__stamp {
  margin-top: 0.3rem;
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  color: var(--quiet);
}

/* In the page for a screen reader, out of sight. */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Three things the markup asks for that no rule had ever reached.
   ---------------------------------------------------------------------------
   The same fault the stepper had: named in the HTML and the script, never
   styled, so each fell back to whatever the browser does by default. Found by
   diffing the classes this page uses against the ones this file defines. */

/* What is in the basket, above the breakdown that itemises it. */
.basket-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.85rem 0 0.35rem;
  color: var(--quiet);
  font-size: 0.85rem;
}
.basket-total__amount {
  font-style: italic;
  font-weight: 800;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* "Opening your UPI app…" and the like: a running commentary on a payment,
   so it is quiet and it is not an error. */
.pay-status {
  font-size: 0.85rem;
  color: var(--quiet);
  text-align: center;
}

/* YOUR ORDER — the app's `_OrderBlock`.
   ---------------------------------------------------------------------------
   Added 22 September 2026, when a real order at a real stall showed a number
   and nothing else: the server was sending `order.lines` and the page threw
   them away. See js/ticket-order.js.

   A list rather than a table: two columns of two or three rows do not need
   table semantics, and a <ul> reads to a screen reader as "list, 2 items",
   which is what somebody checking their own ticket wants confirmed. PAID WITH
   and TOTAL are a <dl>, because they are labels with values. */
.token-order {
  margin-top: 2rem;
}

.token-order__title {
  margin-bottom: 1.1rem;
  font-size: 0.95rem;
  font-weight: 800;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text);
}

.token-order__lines {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
}

.token-order__line {
  display: flex;
  /* The dish left, the money right, and the gap between them elastic: a long
     dish name pushes the price nowhere, because the price column is what the
     eye runs down. */
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.4rem 0;
}

.token-order__name {
  /* Wraps rather than truncating. A dish somebody paid for is not something to
     replace with an ellipsis on a narrow phone. */
  min-width: 0;
  text-align: left;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
}

.token-order__count {
  font-family: var(--display);
  font-style: italic;
  font-weight: 600;
  color: var(--quiet);
}

.token-order__price {
  white-space: nowrap;
  font-family: var(--display);
  font-style: italic;
  font-weight: 600;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  color: var(--quiet);
}

.token-ledger {
  margin: 0;
}
.token-ledger__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 0.55rem 0;
}
.token-ledger dt {
  font-size: 0.8rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--quiet);
}
.token-ledger dd {
  margin: 0;
  font-family: var(--display);
  font-style: italic;
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  color: var(--text);
}
/* GST & PLATFORM FEE — a ledger row that opens the breakdown. The whole row
   is the target, the app's rule: a small glyph is not something anybody can
   hit on purpose. The button inside only has to look like the label. */
.token-ledger__fee {
  cursor: pointer;
}
.token-fee-button {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
}
/* The ⓘ, drawn rather than a font glyph, so it is the same circle on every
   phone. */
.token-fee-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.05rem;
  height: 1.05rem;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 0.7rem;
  font-weight: 700;
  font-style: normal;
  letter-spacing: 0;
  text-transform: none;
}
.token-ledger__fee dd {
  font-weight: 600;
  color: var(--quiet);
}

/* PAID with nothing to name: a lit tick, the one live fact on the row. */
.token-ledger dd.is-tick {
  font-style: normal;
  color: var(--lit);
}
.token-ledger dd.token-ledger__total {
  font-size: 1.65rem;
  font-weight: 900;
}

/* The reconnecting line.
   ---------------------------------------------------------------------------
   Deliberately not --danger. Nothing has gone wrong with this customer's place
   in the line — the page simply cannot reach the server for the moment, and an
   alarming colour over a number that is probably still right would be the page
   lying in the other direction. It reads as small print that is being insisted
   on: the quiet white turned up, and a rule above it so the eye registers that
   the figure above has a caveat attached. */
.token-stale {
  color: var(--text);
  opacity: 0.75;
  font-size: 0.8125rem;
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--edge);
}

/* The fee explainer -------------------------------------------------------

   Same words as the customer app's fee_breakdown_sheet.dart, and the same job:
   a customer at a counter deciding whether a number is fair. A native <dialog>
   does the modal, the focus trap and Escape for nothing, which matters on the
   one surface whose brief is to load on a weak connection.
   --------------------------------------------------------------------------- */
/* Clickable words inside a bill row.
   `#join-form .fee-link` rather than `.fee-link`: the rule it has to beat is
   `#join-form button`, which carries the green primary-action fill. An ID and
   a class outrank an ID and a type, which is the whole reason for the prefix —
   without it the fee line renders as the pay button. */
#join-form .fee-link {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  /* The affordance, and all of it: underlined text reads as something to
     press without borrowing a control's chrome. */
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}

/* The glyph only advertises that there is more to read — the label and the
   figure are what is actually read, which is why this is small and quiet
   rather than an icon button standing on its own. */
.fee-breakdown__more {
  display: inline-grid;
  place-items: center;
  width: 0.95rem;
  height: 0.95rem;
  margin-left: 0.4rem;
  border: 1px solid var(--quiet);
  border-radius: 50%;
  font-size: 0.6rem;
  line-height: 1;
  color: var(--quiet);
  vertical-align: 1px;
}

.fee-sheet {
  width: min(28rem, calc(100vw - 2rem));
  padding: 1.5rem;
  border: 1px solid var(--edge);
  border-radius: 16px;
  background: var(--ground);
  color: var(--text);
}
.fee-sheet::backdrop {
  background: rgba(0, 0, 0, 0.6);
}
.fee-sheet__title {
  margin: 0 0 1.25rem;
  font-size: 0.8rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.fee-sheet__row {
  margin-bottom: 1.1rem;
}
.fee-sheet__line {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: baseline;
}
.fee-sheet__label {
  font-size: 0.95rem;
}
.fee-sheet__amount {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}
.fee-sheet__note {
  margin: 0.3rem 0 0;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--quiet);
}
.fee-sheet__line--total {
  margin-top: 1.25rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--edge);
  font-weight: 800;
}
.notice__message {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--text);
}

.fee-sheet__close {
  width: 100%;
  margin-top: 1.5rem;
  padding: 0.8rem;
  font: inherit;
  font-weight: 700;
  color: var(--text);
  background: none;
  border: 1px solid var(--edge);
  border-radius: 12px;
  cursor: pointer;
}

/* The fee breakdown, shown only for pass_through vendors ------------------

   Quiet by default and emphatic only on the total: the customer already knows
   what the food costs, and the number they need to recognise is the one their
   card will be charged. */
.fee-breakdown {
  margin: 0.75rem 0 0;
  padding: 0.75rem 0.85rem;
  background: rgba(244, 239, 228, 0.04);
  border-radius: 10px;
  font-size: 0.9rem;
}
.fee-breakdown__row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.2rem 0;
  color: var(--quiet);
}
/* Every digit the same width, so the amounts form a column instead of
   shuffling as the figures change. The two-decimal formatting in
   formatBillPrice puts the points in line; this keeps the digits there. */
.fee-breakdown__row > span:last-child {
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.fee-breakdown__row--total {
  margin-top: 0.4rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--edge);
  color: var(--text);
  font-weight: 700;
}

/* ---------------------------------------------------------------------------
   The small print under a menu, and the stall's FSSAI number.

   Deliberately quiet: it is a legal display duty, not something anybody
   scanning a QR code at a stall came to read. It sits at the very bottom, in
   turned-down white, at a size that is legible without competing with the
   dish prices above it.

   Kept in step with apps/customer_app/lib/widgets/menu_footer.dart — same
   three sentences, same order. Two surfaces disagreeing about their own small
   print is the sort of thing nobody notices until it matters.
   --------------------------------------------------------------------------- */
.menu-footer {
  /* Pushed to the bottom of the view rather than sitting directly under the
     content above it: on a stall with three dishes the small print would
     otherwise stop halfway down the screen with nothing beneath it. `auto`
     collapses to nothing the moment the page is long enough to scroll, so a
     full menu is unaffected. The 2rem is the floor. */
  margin-top: auto;
  /* No border. There was a rule here, directly under the pay button, removed
     on 21 September 2026 at the owner's request. The 2rem below already sets
     the small print apart, and a line immediately under the one control that
     takes money read as a boundary the customer had to cross rather than as
     the end of the form. */
  padding-top: 2rem;
}
.menu-footer ul {
  margin: 0;
  /* Room for the marker, which sits outside the text column so the wrapped
     second line of a bullet lines up under the first. */
  padding-left: 1.1rem;
  color: var(--quiet);
  font-size: 0.78rem;
  line-height: 1.5;
}
.menu-footer li + li {
  margin-top: 0.55rem;
}
.fssai {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--edge);
  color: var(--quiet);
  font-size: 0.82rem;
  /* Fourteen digits read as a number to be checked against a certificate, so
     they get the same tabular figures the amounts elsewhere use. */
  font-variant-numeric: tabular-nums;
}
.fssai__mark {
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--text);
}
/* Height-constrained rather than width-constrained, so a mark supplied at a
   different aspect ratio sits on the same line as the number instead of
   deciding how tall the row is. */
.fssai__img {
  /* Taller than the text beside it — see the same constant in
     menu_footer.dart: the mark's letterforms sit in the middle of the box,
     under a rule and over a descender, so matching the text's cap height
     renders it smaller than the number it belongs to. */
  height: 19px;
  width: auto;
  /* The official mark is black on transparent, and this board is dark — so
     the artwork is forced to a flat white and then settled to the same value
     as the text beside it. brightness(0) crushes every colour to black and
     invert(1) lifts it to white, which means the mark comes out right
     whether the supplied file is black, white, or the green-and-white
     version, rather than depending on which one somebody downloaded. */
  filter: brightness(0) invert(1);
  opacity: 0.72;
}
