/* app.html: the tool. Enter the Key, then take or choose a photo of a menu; the page turns it into a dish list.
   Look: 批注本, an annotated book, the same as the landing page (landing.css). Two levels, body and
   annotation: what is copied from the menu (original names, prices) is set in Newsreader in the body
   position; what the tool adds in Chinese sits beside a vermilion rule, the way a note sits beside a line
   of a book. Vermilion is kept for those rules, the seal, the current category, marks that warn and the
   diner's own quantities and choices. Actions are ink. 1px rules, square corners, no shadows, no gradients.
   Colours and fonts are the tokens in :root; the dark scheme only replaces the colours. */
:root {
  --desk: #E9E4D8;
  --paper: #F6F2E8;
  --ink: #1F1B16;
  --muted: #6A6255;
  --rule: #CFC8B8;
  --red: #B4321E;
  /* A filled vermilion mark with paper-coloured text is the same pair in both colour schemes. */
  --seal: #B4321E;
  --on-seal: #F6F2E8;
  /* A QR code is dark modules on a light ground in both colour schemes: many phone cameras do not read one
     the other way round. The ink of the light scheme on white. */
  --qr-ink: #1F1B16;
  --qr-ground: #FFFFFF;
  --container: 1040px;
  --han: "Noto Serif SC", "Songti SC", "STSong", "Source Han Serif SC", "Noto Serif CJK SC", "SimSun", serif;
  --latin: "Newsreader", "Noto Serif SC", Georgia, "Songti SC", "STSong", "Source Han Serif SC", "Noto Serif CJK SC", "SimSun", serif;
}
@media (prefers-color-scheme: dark) {
  :root {
    --desk: #12100D;
    --paper: #1B1814;
    --ink: #EFE9DC;
    --muted: #A69E8F;
    --rule: #3D382F;
    --red: #E8604A;
    color-scheme: dark;
  }
}
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html { height: 100%; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
html, body { overflow-x: clip; }
/* Newsreader has no Chinese characters, so in running text it sets the Latin letters and the numbers and
   Noto Serif SC sets the rest. Names and headings in heavy Chinese type use Noto Serif SC throughout. */
body {
  height: 100%;
  margin: 0;
  background: var(--desk);
  color: var(--ink);
  font-family: var(--latin);
  font-size: 16px;
  line-height: 1.6;
}
button { font: inherit; color: inherit; -webkit-tap-highlight-color: transparent; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.app {
  position: relative;
  max-width: var(--container);
  height: 100vh;
  height: 100dvh;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--paper);
}
@media (min-width: 1041px) {
  .app { border-inline: 1px solid var(--rule); }
}

/* header */
.top {
  flex: none;
  padding-top: calc(12px + env(safe-area-inset-top, 0px));
  padding-bottom: 12px;
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  border-bottom: 1px solid var(--ink);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
}
/* The name of the tool, with the seal of the landing page before it, is the link to the landing page. The
   seal is shown where the seal, the name and the three controls fit on one row. */
.top__title {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-family: var(--han);
  font-size: 22px;
  line-height: 30px;
  font-weight: 900;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.top__home {
  min-height: 44px;
  max-width: 100%;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  text-decoration: none;
  vertical-align: top;
}
.top__seal {
  display: none;
  flex: none;
  width: 28px;
  height: 28px;
  background: var(--seal);
  color: var(--on-seal);
  font-family: var(--han);
  font-size: 19px;
  line-height: 1;
  font-weight: 900;
  letter-spacing: 0;
}
@media (min-width: 390px) {
  .top__seal { display: grid; place-items: center; }
}
.top__home:focus-visible { outline-offset: 2px; }
.top__actions { flex: none; display: flex; align-items: center; gap: 8px; }
.top__actions .shoot { display: flex; align-items: center; gap: 8px; }
.top__actions .btn { padding-inline: 12px; }
/* The Key control, which opens the Key dialog: the word itself, underlined. Its 44px box reaches into the
   space at both sides, so the word ends at the page margin and the box touches nothing. */
.keylink {
  min-width: 44px;
  min-height: 44px;
  margin: 0 -6px 0 -2px;
  padding: 4px 6px 0 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  color: var(--ink);
  font-size: 17px;
  line-height: 24px;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  cursor: pointer;
}
.keylink:focus-visible { outline-offset: -2px; }

/* The 2px at the top sets Chinese words at the middle of a button: the line is measured with Newsreader,
   which stands higher on it than the Chinese type does. */
.btn {
  min-width: 44px;
  min-height: 44px;
  padding: 2px 14px 0;
  border: 1px solid var(--ink);
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 24px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
}
.btn--primary { background: var(--ink); color: var(--paper); }
/* A control that is rarely needed is set as underlined words, like the Key link. It is four characters
   wide: the longer words it shows while it waits for a second tap take two lines, so nothing beside it moves. */
.btn--quiet {
  max-width: 4.25em; padding-inline: 0; border: 0; font-weight: 400;
  text-align: right; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px;
}
/* 清空 and 清空菜单 ask for a second tap. While they wait for it they are vermilion. */
.btn.is-armed { border-color: var(--red); color: var(--red); }
.sr-only:focus-visible + .btn { outline: 2px solid var(--red); outline-offset: 3px; }

/* status line */
.status {
  position: relative;
  flex: none;
  padding-block: 10px;
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  overflow-x: clip;
}
/* With nothing to say (the start screen before a photo is chosen) the line takes no room. It stays in the
   page, so that what it says next is read out. */
.status--quiet { padding-block: 0; border-bottom: 0; }
/* The words take what the buttons leave, so the buttons stand together at the right. */
.status__body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.status__text { margin: 0; font-size: 16px; line-height: 24px; overflow-wrap: anywhere; text-wrap: balance; }
.status__note { margin: 0; color: var(--muted); font-size: 14px; line-height: 22px; overflow-wrap: anywhere; text-wrap: balance; }
/* A word in quotes (点“画图”) is set in the Chinese face, whose quotes are full width, and kept on one line. */
.quoted { font-family: var(--han); white-space: nowrap; }
/* A message about something that went wrong is a note in the margin: vermilion, a vermilion rule at its left.
   When only some dishes did not get their picture, the menu is fine: the line keeps the ink and has the rule. */
.status[data-kind="error"] .status__body, .status[data-kind="warn"] .status__body { padding-left: 12px; border-left: 2px solid var(--red); }
.status[data-kind="error"] .status__text { color: var(--red); }
.status .btn { flex: none; }
/* While a photo is being read, a vermilion rule moves along the bottom of the status line. */
.status[data-kind="busy"]::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 28%;
  height: 2px;
  background: var(--red);
  animation: reading 1.8s linear infinite;
}
@keyframes reading {
  from { transform: translateX(-100%); }
  to { transform: translateX(358%); }
}
@media (prefers-reduced-motion: reduce) {
  .status[data-kind="busy"]::after { width: 100%; animation: none; }
}

/* rail and list, and the bar below them */
.work { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
.body { flex: 1 1 0; min-height: 0; display: flex; }
.rail {
  position: relative;
  flex: none;
  width: clamp(76px, 19vw, 168px);
  padding-left: env(safe-area-inset-left, 0px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--desk);
  border-right: 1px solid var(--rule);
}
.rail__btn {
  display: block;
  width: 100%;
  min-height: 56px;
  padding: 12px 6px 12px 4px;
  border: 0;
  border-left: 2px solid transparent;
  border-bottom: 1px solid var(--rule);
  background: transparent;
  color: var(--ink);
  font-family: var(--han);
  font-size: 15px;
  line-height: 22px;
  text-align: center;
  overflow-wrap: anywhere;
  /* A name that needs two lines is split into lines of like length, so no character is left alone. */
  text-wrap: balance;
  cursor: pointer;
}
.rail__btn[aria-current="true"] { background: var(--paper); border-left-color: var(--red); color: var(--red); font-weight: 900; }
.rail__btn:active { background: var(--paper); }
.rail__btn:focus-visible { outline-offset: -4px; }

.list {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  padding-block: 16px 24px;
  padding-left: 16px;
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  overflow-y: auto;
  overscroll-behavior: contain;
}
.cat { padding-bottom: 24px; }
/* The end of the list: what holds for every menu, in small muted type after the last category, like the
   colophon of a book. A line is kept to about 40 characters on a wide screen. */
.list__end { max-width: 40em; color: var(--muted); font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
.list__end p { margin: 0; text-wrap: pretty; }
/* A category opens like a running head: Chinese at the left, the menu's own word at the right, an ink rule below. */
.cat__head { padding-bottom: 6px; border-bottom: 1px solid var(--ink); }
.cat__head h2 {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 12px;
  font-family: var(--han);
  font-size: 20px;
  line-height: 28px;
  font-weight: 900;
  overflow-wrap: anywhere;
}
.cat__orig { font-family: var(--latin); font-size: 19px; font-style: italic; font-weight: 400; }
.dishes {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
  column-gap: 32px;
}
.dish {
  min-width: 0;
  padding: 16px 0 6px;
  border-bottom: 1px solid var(--rule);
  display: flex;
  gap: 10px;
}
/* The AI's drawing of a dish is framed in ink. Without one the frame is a hairline and says, in the muted
   colour, what is happening: 排队中 while the dish waits its turn, 正在画 (in ink, while its bottom rule is
   inked from left to right again and again) while it is drawn, 没画成 over the button when a drawing
   failed. The button 画图 is the whole frame, its word underlined in ink like the other controls set as
   words; nothing else in the frame is underlined, so the moving rule is the frame's own edge. */
.dish__pic {
  position: relative;
  flex: none;
  width: 80px;
  height: 80px;
  margin: 0;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 0;
  background: transparent;
  overflow: hidden;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  font-family: var(--han);
  font-size: 14px;
  line-height: 20px;
  font-weight: 400;
}
.dish__pic:has(img) { border-color: var(--ink); }
.dish__pic img { display: block; width: 100%; height: 100%; object-fit: cover; }
.dish__draw { cursor: pointer; }
.dish__drawlabel { color: var(--ink); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
.dish__failed { font-size: 13px; line-height: 18px; }
.dish__pic--drawing { color: var(--ink); overflow: visible; }
.dish__pic--drawing::after {
  content: "";
  position: absolute;
  left: -1px;
  right: -1px;
  bottom: -1px;
  height: 1px;
  background: var(--ink);
  transform-origin: left;
  animation: drawing 1.6s ease-in-out infinite;
}
@keyframes drawing {
  0% { transform: scaleX(0); opacity: 1; }
  70% { transform: scaleX(1); opacity: 1; }
  100% { transform: scaleX(1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .dish__pic--drawing::after { content: none; animation: none; }
}
/* On a shared menu opened without a Key, a dish that has no drawing yet says so in the hairline frame: it
   cannot be drawn here. */
.dish__pic--none { font-size: 13px; line-height: 18px; }
.dish__body { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
.dish__names { display: flex; flex-direction: column; }
.dish__zh { font-family: var(--han); font-size: 18px; line-height: 26px; font-weight: 900; overflow-wrap: anywhere; }
/* The name as the menu prints it: what the diner says or points at. */
.dish__orig { font-size: 18px; line-height: 24px; overflow-wrap: anywhere; }
/* On a menu of several photos, the photo the dish is printed on: a reference in small muted type under the names. */
.dish__photo { color: var(--muted); font-family: var(--han); font-size: 13px; line-height: 20px; }
/* The note on a dish: description and marks share one vermilion rule. */
.dish__desc, .dish__marks { padding-left: 8px; border-left: 2px solid var(--red); }
.dish__desc { margin: 8px 0 0; font-size: 15px; line-height: 24px; overflow-wrap: anywhere; text-wrap: pretty; }
.dish__marks { margin-top: 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
.dish__desc + .dish__marks { margin-top: 0; padding-top: 4px; }
.dish__contains { color: var(--muted); font-family: var(--han); font-size: 13px; line-height: 22px; }
/* A quiet line on a dish, outside the note: what it can be chosen with (可选：肉类、换米饭) and the hint
   about this dish on this menu. */
.dish__aside { margin: 8px 0 0; color: var(--muted); font-family: var(--han); font-size: 13px; line-height: 20px; overflow-wrap: anywhere; text-wrap: pretty; }
.dish__aside + .dish__aside { margin-top: 2px; }
/* A dish with choices opens its chooser when it is tapped. */
.dish--choices { cursor: pointer; }
.tag {
  flex: none;
  padding: 0 6px;
  border: 1px solid var(--ink);
  font-family: var(--han);
  font-size: 13px;
  line-height: 20px;
  white-space: nowrap;
}
/* The two marks that warn are vermilion: how hot a dish is (filled, like the seal) and 不确定 (outlined). */
.tag--hot { border-color: var(--seal); background: var(--seal); color: var(--on-seal); font-weight: 600; }
.tag--unsure { border-color: var(--red); color: var(--red); }
/* Price, a dotted leader as on a printed menu, then the number of portions. */
.dish__foot {
  min-height: 44px;
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 8px;
  /* The leader below measures the foot: 100cqw is its width. */
  container-type: inline-size;
}
.dish__price { padding-block: 9px; font-size: 19px; line-height: 26px; font-weight: 600; overflow-wrap: anywhere; }
/* The plus of a dish must not move when the first portion is added: a second tap at the same spot has to
   land on it again. So the stepper asks for the width of all three of its parts (44 + 20 + 44) from the
   start, and takes what the price leaves of the line. Whether the foot is one line or, on a narrow phone
   with a long price, two is then the same before and after the first tap. min-width: 0 keeps the
   stepper's own contents out of that decision. */
.dish .stepper { flex: 1 1 108px; min-width: 0; margin-left: 0; justify-content: flex-end; }
/* The leader is the stepper's own first part. It fills what the buttons leave free, up to the plus while
   nothing is chosen, and ends 8px before the first button. --shown is the width of the buttons that are
   shown, --room what they and those 8px leave. The leader has no width, so it is not drawn, in two cases:
   when it would be shorter than 16px, and when the stepper has a line to itself (it is then 8px wider than
   the foot, by its margin) and there is no price beside it to lead from. */
.dish__foot:has(.dish__price) .stepper::before {
  --shown: 108px;
  content: "";
  flex: 1 1 0;
  min-width: 0;
  height: 6px;
  margin-top: 4px;
  border-bottom: 1px dotted var(--ink);
}
.dish__foot:has(.dish__price) .stepper:has(.step--minus[hidden])::before { --shown: 44px; }
/* A dish with choices has no minus: the number of its portions, once there are some, and the plus. */
.dish__foot:has(.dish__price) .stepper--choices::before { --shown: 64px; }
.dish__foot:has(.dish__price) .stepper--choices:has(.step__qty[hidden])::before { --shown: 44px; }
@supports (width: 1cqw) {
  .dish__foot:has(.dish__price) .stepper::before {
    --room: calc(100% - var(--shown) - 8px);
    flex: none;
    width: clamp(0px, min((100cqw + 7px - 100%) * 9999, (var(--room) - 16px) * 9999), var(--room));
    margin-right: 8px;
  }
}

.stepper { order: 2; flex: none; margin-left: auto; margin-right: -8px; display: flex; align-items: center; }
.step {
  flex: none; width: 44px; height: 44px; padding: 0; border: 0;
  background: transparent; display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.step__disc { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; }
.step--plus .step__disc { background: var(--ink); color: var(--paper); }
.step--minus .step__disc { border: 1px solid var(--ink); }
.step:focus-visible { outline-offset: -4px; }
/* Always the same width, so the buttons at its sides never move. Two digits (the most is 99) need 24px;
   2px at each side lie over the clear edge of the button beside it, which leaves the buttons 20px apart. */
.step__qty {
  flex: none; width: 24px; margin-inline: -2px; color: var(--red); font-size: 19px; line-height: 26px; font-weight: 600;
  font-variant-numeric: tabular-nums; text-align: center;
}

/* 停止 and 清空菜单 side by side (both are there while dishes are drawn) take about 140px, and 分享 beside them
   about 200px. Below 560px that leaves the words of the status line too narrow: at 390px the first line broke
   after 已, at 320px the words were a column of eight characters. There the buttons go on a row of their own
   under the words, at the right: 分享, 停止, 清空菜单. The status line is no taller with 分享 in that row.
   So do 去填写 API Key or 更换 API Key and 清空菜单, which are there together over a menu without a Key, or
   when a reading was refused for the Key. */
@media (max-width: 559px) {
  .status:has(> .btn--quiet:not([hidden])):is(:has(> .status__stop:not([hidden])), :has(> .status__link:not([hidden]))) { flex-wrap: wrap; justify-content: flex-end; row-gap: 4px; }
  .status:has(> .btn--quiet:not([hidden])):is(:has(> .status__stop:not([hidden])), :has(> .status__link:not([hidden]))) .status__body { flex: 1 0 100%; }
}

/* Below 360px the column beside the picture is about 120px: six characters of a description to a line, and
   a price apart from its plus. There the picture stands beside the names only, and the description, the
   marks and the price with its plus take the whole width under them. */
@media (max-width: 359px) {
  .dish { display: grid; grid-template-columns: 80px minmax(0, 1fr); gap: 0 10px; align-items: start; }
  .dish__body { display: contents; }
  .dish__pic { grid-column: 1; grid-row: 1; }
  .dish__names { grid-column: 2; grid-row: 1; }
  .dish__desc, .dish__marks, .dish__aside, .dish__foot { grid-column: 1 / -1; }
}

/* bottom bar */
.bar {
  flex: none;
  padding-top: 10px;
  padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  border-top: 1px solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.bar__text { min-width: 0; display: flex; flex-direction: column; }
.bar__count { font-size: 17px; line-height: 24px; font-weight: 600; }
/* Once something is chosen, the count is vermilion like the quantities in the list. */
.bar__count--some { color: var(--red); }
.bar__total { color: var(--muted); font-size: 15px; line-height: 20px; }
/* 其余 1 份另计 in a total: not broken across lines. */
.nowrap { white-space: nowrap; }
.bar__btn {
  flex: none; min-height: 44px; padding: 0 20px; border: 0; border-radius: 0;
  background: var(--ink); color: var(--paper);
  font-family: var(--han); font-size: 16px; line-height: 24px; font-weight: 600; cursor: pointer;
}

/* order list */
.sheet {
  position: fixed;
  inset: auto 0 0 0;
  width: 100%;
  max-width: 560px;
  max-height: 80%;
  margin: 0 auto;
  padding: 0;
  border: 0;
  border-top: 1px solid var(--ink);
  overflow: hidden;
  background: var(--paper);
  color: var(--ink);
  flex-direction: column;
}
.sheet[open] { display: flex; }
/* The page behind the list is dimmed. Written out, not var(): a dialog's backdrop does not inherit
   custom properties in every browser. */
.sheet::backdrop, .sheet-backdrop { background: rgba(31, 27, 22, 0.5); }
.sheet-backdrop { position: fixed; inset: 0; }
@media (prefers-color-scheme: dark) {
  .sheet::backdrop, .sheet-backdrop { background: rgba(0, 0, 0, 0.6); }
}
.sheet__head {
  flex: none;
  padding: 8px 4px 8px 16px;
  border-bottom: 1px solid var(--ink);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.sheet__head h2 { margin: 0; font-family: var(--han); font-size: 20px; line-height: 28px; font-weight: 900; }
.iconbtn {
  flex: none; width: 44px; height: 44px; padding: 0; border: 0; background: transparent; color: var(--ink);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.sheet__scroll { flex: 1 1 auto; min-height: 0; padding: 0 16px; overflow-y: auto; overscroll-behavior: contain; }
.sheet__hint { margin: 0; padding: 12px 0 4px; color: var(--muted); font-size: 14px; line-height: 22px; }
.sheet__list { margin: 0; padding: 0; list-style: none; }
/* A line of the list is set like a line of the menu: the original name, a leader, the number of portions.
   The Chinese name is the note under it. */
.sheet__row {
  padding: 6px 0 10px;
  border-bottom: 1px solid var(--rule);
  display: flex; align-items: flex-start; gap: 8px;
}
/* The last line of the list ends at the ink rule above the total, so it needs no hairline of its own.
   It keeps the hairline when the text for copying by hand follows it. */
.sheet__list:has(+ .sheet__note[hidden]) .sheet__row:last-child { border-bottom: 0; }
.sheet__row::after {
  content: "";
  order: 1;
  flex: 1 1 0;
  min-width: 0;
  height: 29px;
  border-bottom: 1px dotted var(--ink);
}
.sheet__row .stepper { margin-left: 0; }
.sheet__names { flex: 0 1 auto; min-width: 0; padding-top: 8px; display: flex; flex-direction: column; }
.sheet__orig { font-size: 21px; line-height: 28px; overflow-wrap: anywhere; }
/* The choices of a line, in the menu's words, under the dish like the lines of a kitchen ticket. */
.sheet__choices { font-size: 17px; line-height: 24px; overflow-wrap: anywhere; }
.sheet__zh {
  margin-top: 4px; padding-left: 10px; border-left: 2px solid var(--red);
  font-size: 15px; line-height: 22px; overflow-wrap: anywhere;
}
.sheet__empty { margin: 0; padding: 28px 0; color: var(--muted); font-size: 16px; line-height: 26px; text-align: center; }
.sheet__note { margin: 0; padding: 12px 0 0; color: var(--muted); font-size: 14px; line-height: 22px; }
.sheet__manual {
  display: block; width: 100%; min-height: 132px; margin: 8px 0 12px; padding: 12px;
  border: 1px solid var(--ink); border-radius: 0; background: var(--paper); color: var(--ink);
  font: inherit; font-size: 16px; line-height: 24px; resize: vertical;
}
/* The total stays in place while the lines above it scroll: an ink rule parts the two, like the one under the head. */
.sheet__sum {
  flex: none; margin: 0; padding: 12px 16px 0; border-top: 1px solid var(--ink);
  font-size: 17px; line-height: 24px; font-weight: 600;
}
.sheet__actions {
  flex: none;
  padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: 12px;
}
.sheet__actions .btn--primary { flex: 1 1 0; min-width: 0; }
/* An empty list has no buttons at its foot: the text then keeps clear of a phone's home indicator itself. */
.sheet:has(.sheet__actions[hidden]) .sheet__scroll { padding-bottom: env(safe-area-inset-bottom, 0px); }

/* The chooser of a dish with choices: the sheet of the order list, with the dish at its head. */
.chooser__head { align-items: flex-start; padding-top: 12px; padding-bottom: 12px; }
.chooser__pic { flex: none; width: 56px; height: 56px; border: 1px solid var(--ink); overflow: hidden; }
.chooser__pic img { display: block; width: 100%; height: 100%; object-fit: cover; }
.chooser__names { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.chooser__names h2 { margin: 0; font-family: var(--han); font-size: 20px; line-height: 28px; font-weight: 900; overflow-wrap: anywhere; }
.chooser__orig { margin: 0; font-size: 17px; line-height: 24px; overflow-wrap: anywhere; }
.chooser__head .iconbtn { margin-top: -6px; }
/* Each group opens like a category: its Chinese name, 选一项 or 可多选 beside it, the menu's words at the
   right, an ink rule below. */
.opt { padding: 16px 0 8px; }
.opt + .opt { padding-top: 12px; }
.opt__head {
  margin: 0;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--ink);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 10px;
  font-size: 17px;
  line-height: 26px;
  font-weight: 400;
}
.opt__name { font-family: var(--han); font-weight: 900; }
.opt__pick { color: var(--muted); font-family: var(--han); font-size: 13px; line-height: 20px; }
.opt__printed { margin-left: auto; font-size: 16px; font-style: italic; overflow-wrap: anywhere; }
/* A choice is a line of the group, the whole line a target: the mark, the names, the price at the right.
   The radio button or check box itself is not shown; the mark is drawn for it, ink while it is not chosen,
   vermilion once it is, like the portions the diner has chosen. */
.opt__row {
  position: relative;
  min-height: 48px;
  padding: 11px 0;
  border-bottom: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 12px;
  cursor: pointer;
}
.opt__row:last-child { border-bottom: 0; }
.opt__input { position: absolute; top: 14px; left: 0; width: 20px; height: 20px; margin: 0; opacity: 0; }
.opt__mark { align-self: start; width: 20px; height: 20px; margin-top: 2px; border: 1px solid var(--ink); display: grid; place-items: center; }
.opt__input[type="radio"] + .opt__mark { border-radius: 50%; }
.opt__input:checked + .opt__mark { border-color: var(--red); }
.opt__input[type="radio"]:checked + .opt__mark::after { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--red); }
.opt__input[type="checkbox"]:checked + .opt__mark { background: var(--seal); border-color: var(--seal); }
.opt__input[type="checkbox"]:checked + .opt__mark::after {
  content: ""; width: 10px; height: 5px; margin-top: -3px;
  border-left: 2px solid var(--on-seal); border-bottom: 2px solid var(--on-seal); transform: rotate(-45deg);
}
.opt__input:focus-visible + .opt__mark { outline: 2px solid var(--red); outline-offset: 3px; }
.opt__names { min-width: 0; font-size: 16px; line-height: 24px; overflow-wrap: anywhere; }
.opt__zh { font-family: var(--han); }
.opt__input:checked ~ .opt__names .opt__zh { font-weight: 600; }
.opt__names .opt__orig { margin-left: 4px; }
.opt__price { max-width: 9em; font-size: 16px; line-height: 24px; font-variant-numeric: tabular-nums; text-align: right; overflow-wrap: anywhere; }
/* The foot: the number of portions and 加入清单 with the price of the line, under an ink rule. */
.chooser__actions { border-top: 1px solid var(--ink); }
.chooser__qty { flex: none; margin-left: -8px; }
.chooser__qty .stepper { margin: 0; }
.chooser__add { flex: 1 1 0; min-width: 0; gap: 10px; }
.chooser__price { font-family: var(--latin); font-variant-numeric: tabular-nums; }

/* The share sheet: the sheet of the order list, with the QR code of the link at the left (at the top on a
   phone) and the code beside it in large spaced figures, under the label 分享码. The QR code is drawn on its
   own white ground, quiet zone included, and framed in ink like a dish's drawing. One sentence says what
   the link does, as a note with the vermilion rule. */
.share__scroll { padding-top: 16px; padding-bottom: 16px; }
.share__card { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.share__qr { flex: none; display: grid; place-items: center; min-width: 202px; min-height: 202px; border: 1px solid var(--rule); color: var(--muted); font-family: var(--han); font-size: 14px; line-height: 22px; }
.share__qr:has(svg[role="img"]) { border-color: var(--ink); background: var(--qr-ground); }
.share__qr > * { grid-area: 1 / 1; }
.share__qr svg { display: block; }
.qr__ground { fill: var(--qr-ground); }
.qr__dark { fill: var(--qr-ink); }
.share__words { min-width: 0; align-self: stretch; display: flex; flex-direction: column; align-items: center; }
.share__label { margin: 0; color: var(--muted); font-family: var(--han); font-size: 12px; line-height: 18px; }
/* The letter spacing also follows the last character: the margin takes it back, so the code stands centred. */
.share__code {
  margin: 0 -0.2em 0 0; font-family: var(--latin); font-size: 40px; line-height: 52px; font-weight: 600;
  letter-spacing: 0.2em; font-variant-numeric: lining-nums tabular-nums; white-space: nowrap;
}
.share__say, .share__wait { align-self: stretch; margin: 10px 0 0; padding-left: 10px; border-left: 2px solid var(--red); font-size: 15px; line-height: 24px; overflow-wrap: anywhere; text-wrap: pretty; }
.share__wait { margin-top: 0; padding-top: 4px; color: var(--muted); font-size: 14px; line-height: 22px; }
.share__say:empty { display: none; }
/* What went wrong: a note in the margin, vermilion, as in the status line. */
.share__error { margin: 0; padding-left: 12px; border-left: 2px solid var(--red); color: var(--red); font-size: 16px; line-height: 24px; overflow-wrap: anywhere; }
.share__manual {
  display: block; width: 100%; min-height: 44px; margin: 8px 0 0; padding: 0 10px;
  border: 1px solid var(--ink); border-radius: 0; background: var(--paper); color: var(--ink);
  font: inherit; font-size: 16px; line-height: 24px;
}
.share .sheet__actions { border-top: 1px solid var(--ink); }
.share .sheet__actions .btn { flex: 1 1 0; }
@media (min-width: 480px) {
  .share__card { flex-direction: row; align-items: center; gap: 20px; }
  .share__words { align-items: flex-start; align-self: center; }
}

/* The field for a code at the end of the dish list, after its footer: a label in small type, the field
   and 打开. A code is upper case, so what is typed is shown in capitals; spaces and dashes may stay. */
.code { max-width: 40em; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rule); }
.code__label { margin: 0 0 6px; color: var(--muted); font-family: var(--han); font-size: 12px; line-height: 18px; }
.code__row { max-width: 320px; display: flex; gap: 8px; }
.code__input {
  flex: 1 1 0; min-width: 0; min-height: 44px; margin: 0; padding: 0 10px;
  border: 1px solid var(--ink); border-radius: 0; background: var(--paper); color: var(--ink);
  font-family: var(--latin); font-size: 18px; line-height: 24px; letter-spacing: 0.12em; text-transform: uppercase;
}
.code__input::placeholder { color: var(--muted); opacity: 1; }
.code__error { margin: 6px 0 0; color: var(--red); font-size: 14px; line-height: 22px; overflow-wrap: anywhere; }
.code__hint { margin: 6px 0 0; color: var(--muted); font-family: var(--han); font-size: 14px; line-height: 22px; }

/* ---------- the start screen ----------
   While there is no menu the page is a single page of the book: one column, at most 480px wide, in the
   middle of the paper. Without a Key it holds a heading, one line set as a note (the vermilion rule) and the
   Key form in an ink frame, as on the landing page before. With a Key it holds one line in large type and the
   two buttons 拍菜单 and 相册, wide and tall, in the middle of the screen, where a thumb reaches them. Under
   either, at the foot of the page and parted by a hairline, the field for a share code, quieter: a label in
   the muted colour and the outlined 打开. */
.start { flex: 1 1 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.start__page {
  min-height: 100%;
  max-width: calc(480px + 32px);
  margin: 0 auto;
  padding-top: 28px;
  padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  padding-left: max(16px, env(safe-area-inset-left, 0px));
  padding-right: max(16px, env(safe-area-inset-right, 0px));
  display: flex;
  flex-direction: column;
}
.start__step { display: flex; flex-direction: column; }
.start__title {
  margin: 0;
  font-family: var(--han);
  font-size: 26px;
  line-height: 38px;
  font-weight: 900;
  letter-spacing: 0.02em;
}
/* Each clause of a heading, and each sentence of a note, is one piece: a line that has to break breaks
   between them, and a piece too long for a line breaks inside itself only. */
.start__title > span, .start__line > span, .keysheet__why > span { display: inline-block; }
.start__line { margin: 12px 0 0; padding-left: 12px; border-left: 2px solid var(--red); font-family: var(--han); font-size: 15px; line-height: 24px; overflow-wrap: anywhere; }
.start__form { margin-top: 24px; }
/* The photo step stands in the middle of what the field for a share code leaves of the screen, a little
   below the middle: the buttons come at the height of a thumb. */
.start__step--photo { flex: 1 0 auto; justify-content: center; padding-top: 4vh; }
.start__shoot { margin-top: 28px; }
.start .shoot { display: flex; flex-direction: column; gap: 12px; }
.start .shoot .btn { min-height: 56px; padding-inline: 16px; font-size: 18px; line-height: 26px; }
.start .shoot .btn--primary { min-height: 60px; font-size: 19px; letter-spacing: 0.08em; }
.start__code { margin-top: auto; padding-top: 32px; }
.start .code { max-width: none; margin-top: 0; padding-top: 16px; }
.start .code__label { color: var(--ink); font-size: 15px; line-height: 22px; }
@media (min-width: 600px) {
  .start__page { padding-top: 56px; }
  .start__title { font-size: 30px; line-height: 44px; }
  .start .shoot { flex-direction: row; }
  .start .shoot .btn { flex: 1 1 0; }
}

/* ---------- the Key form ----------
   The form of the landing page, moved here: an ink frame; the label, the field on an ink rule with 显示 at
   its right; what went wrong as a note in vermilion under the field; 在这台设备上记住; the one filled button;
   the hint in the muted colour. In the Key dialog it has no frame of its own: the sheet is its frame. */
.keybox { margin: 0; padding: 16px 16px 18px; border: 1px solid var(--ink); font-size: 16px; line-height: 1.6; }
.field__label { display: block; margin: 0; font-family: var(--han); font-size: 16px; line-height: 24px; }
.field__row { display: flex; align-items: center; gap: 10px; margin-top: 6px; border-bottom: 1px solid var(--ink); }
.field__row:has([aria-invalid="true"]) { border-bottom: 2px solid var(--red); margin-bottom: -1px; }
#api-key {
  flex: 1 1 auto; min-width: 0; height: 52px; margin: 0; padding: 0 2px;
  border: 0; border-radius: 0; background: transparent; color: var(--ink);
  font-family: var(--latin); font-size: 20px;
}
#api-key::placeholder { color: var(--muted); opacity: 1; }
#api-key:focus-visible { outline-offset: 2px; }
.field__toggle {
  flex: none; min-width: 60px; height: 44px; padding: 2px 12px 0; border: 0; border-radius: 0;
  background: transparent; color: var(--ink); font-family: var(--han); font-size: 15px; cursor: pointer;
}
.field__toggle[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
/* What went wrong is a note in the margin: vermilion, a vermilion rule at its left. */
.field__msg { margin: 10px 0 0; padding-left: 12px; border-left: 2px solid var(--red); color: var(--red); font-family: var(--han); font-size: 15px; line-height: 1.6; overflow-wrap: anywhere; }
.field__msg:empty { display: none; }
.check { display: flex; align-items: center; gap: 10px; width: fit-content; min-height: 44px; margin-top: 6px; font-family: var(--han); font-size: 15px; cursor: pointer; }
.check input {
  -webkit-appearance: none; appearance: none; flex: none; display: grid; place-content: center;
  width: 20px; height: 20px; margin: 0; border: 1px solid var(--ink); border-radius: 0; background: transparent; cursor: pointer;
}
.check input::before { content: ""; width: 11px; height: 6px; border-left: 2px solid var(--paper); border-bottom: 2px solid var(--paper); transform: translateY(-1px) rotate(-45deg); visibility: hidden; }
.check input:checked { background: var(--ink); }
.check input:checked::before { visibility: visible; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.keybox .btn, .keysheet .btn { min-height: 50px; }
.keybox .btn--primary { flex: 1 1 auto; font-size: 17px; }
/* 正在检查… is shorter than 检查 Key 并开始: the button keeps its width. While it checks it is the muted
   colour, its words still readable. */
#key-submit { min-width: 9.5em; }
#key-submit:disabled { opacity: 1; border-color: var(--muted); background: var(--muted); color: var(--paper); cursor: progress; }
#key-save-anyway { flex: 1 1 auto; }
/* Where 不检查，保存并继续 does not fit beside the button it goes above it, and the button stays where it was:
   a second click on the same spot, meant as "try again", lands on 检查 again. */
@media (min-width: 601px) {
  #key-form .actions { flex-wrap: wrap-reverse; }
}
.hint { margin: 12px 0 0; color: var(--muted); font-family: var(--han); font-size: 14px; line-height: 1.7; }
/* A sentence of a hint is one piece: it moves to the next line whole. */
.hint span { display: inline-block; }
.hint a { color: var(--ink); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
.saved__key { display: flex; align-items: center; height: 52px; margin: 6px 0 0; border-bottom: 1px solid var(--ink); font-family: var(--latin); font-size: 20px; }

/* ---------- the Key dialog ----------
   The sheet of the order list. With a Key kept: the Key as sk-… and its last 4 characters on an ink rule,
   换一个 Key and 删除这个 Key side by side, the hint. To change the Key or enter one, the Key form under it.
   When a photo waits for the Key, one sentence above says so, as a note with the vermilion rule. */
.keysheet__scroll { padding-top: 16px; padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px)); }
.keysheet__why { margin: 0 0 16px; padding-left: 12px; border-left: 2px solid var(--red); font-family: var(--han); font-size: 15px; line-height: 24px; overflow-wrap: anywhere; }
.keysheet__saved .actions { margin-top: 16px; }
.keysheet__saved .actions .btn { flex: 1 1 auto; }
.keysheet .keybox { padding: 0; border: 0; }
.keysheet__saved:not([hidden]) + .keysheet__form:not(:empty) { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--rule); }

/* A short screen: a phone held sideways, a split screen, or a window enlarged several times (1280x1024 at
   400% is 320x256). The header, the status line and the bar would leave the dish list a strip, or nothing.
   Here the page itself scrolls. The header and the status line scroll away; the dish list is as tall as
   the screen less the bar (65px) and still scrolls inside its own box, which the category buttons rely on;
   the bar stays at the bottom of the screen, and never above the top of the list. The order list may
   take the whole height and scrolls as one piece under its head. */
@media (max-height: 440px) {
  html, body { height: auto; }
  html { scroll-padding-bottom: calc(65px + env(safe-area-inset-bottom, 0px)); }
  .app { height: auto; min-height: 100vh; min-height: 100dvh; overflow: visible; }
  .work { flex: none; display: block; }
  .start { flex: none; overflow: visible; }
  .start__step--photo { padding-top: 0; }
  .body { height: max(180px, calc(100vh - 65px)); height: max(180px, calc(100dvh - 65px - env(safe-area-inset-bottom, 0px))); }
  .bar { position: sticky; bottom: 0; z-index: 1; background: var(--paper); }
  .list, .rail { overscroll-behavior: auto; }
  .sheet { max-height: 100%; overflow-y: auto; overscroll-behavior: contain; }
  .sheet__head { position: sticky; top: 0; z-index: 1; background: var(--paper); }
  .sheet__scroll { flex: none; overflow: visible; }
}

/* With a mouse: a second hairline frame around a button, as on the landing page; a heavier underline
   under the controls that are set as words. */
@media (hover: hover) {
  .btn:not(.btn--quiet):hover:not(:focus-visible), .bar__btn:hover:not(:focus-visible) { outline: 1px solid var(--ink); outline-offset: 3px; }
  .step:hover:not(:focus-visible) .step__disc { outline: 1px solid var(--ink); outline-offset: 2px; }
  .keylink:hover, .btn--quiet:hover, .dish__draw:hover .dish__drawlabel, .hint a:hover { text-decoration-thickness: 2px; }
  .top__home:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 6px; }
  .field__toggle:hover:not([aria-pressed="true"]) { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
  .dish__draw:hover { border-color: var(--ink); }
  .rail__btn:hover { background: var(--paper); }
  .iconbtn:hover:not(:focus-visible) { outline: 1px solid var(--ink); outline-offset: -8px; }
  .opt__row:hover .opt__mark { outline: 1px solid var(--ink); outline-offset: 2px; }
}
@media (prefers-reduced-motion: no-preference) {
  .btn:active:not(:disabled), .bar__btn:active, .iconbtn:active, .step:active .step__disc { transform: translateY(1px); }
}
