/* Aloke — the public menu page a diner sees after scanning the table QR.

   This file must work for EVERY theme in menu_themes.py, so it reads only
   custom properties: --bg --surface --text --muted --brand --accent --line
   --radius --shadow --font --heading. No hardcoded colour, ever.

   Defaults live on `html` (specificity 0,0,1) while the API injects `:root`
   (0,1,0), so the theme always wins even though this file loads first. */

html {
  --bg: #FBF7F1;
  --surface: #FFFFFF;
  --text: #2B2119;
  --muted: #7A6A5C;
  --brand: #8A5A2B;
  --accent: #C8873F;
  --line: #E8DDCE;
  --radius: 18px;
  --shadow: 0 2px 14px rgba(80, 50, 20, .07);
  --font: 'Readex Pro', 'Noto Sans Arabic', 'Segoe UI', system-ui, sans-serif;
  --heading: 'Readex Pro', 'Noto Kufi Arabic', 'Trebuchet MS', sans-serif;

  /* TYPE RAMP. The API emits every one of these on :root (menu_themes.TYPE,
     multiplied by the theme's own --scale). They are mirrored here so this
     file is complete on its own — a themeVars block that failed to arrive must
     not leave the menu rendering at the browser's default sizes.

     These are the published QR-menu figures, not taste: section 22-24px,
     item 18-20px, price 16-18px at the SAME weight as the name it belongs to,
     description 14-16px at 1.5, and nothing anywhere below 14px. Every size in
     this stylesheet is derived from them; there are no loose px sizes left. */
  --scale: 1;
  --size-section: calc(1.45rem * var(--scale));   /* ~23px */
  --size-item: calc(1.15rem * var(--scale));      /* ~18px */
  --size-price: calc(1.05rem * var(--scale));     /* ~17px */
  --size-desc: max(0.875rem, calc(0.95rem * var(--scale)));
  --lh-desc: 1.5;
  --tap: 44px;
  --tracking: 0;

  /* The floor, named once. Anything secondary (a currency code, a tag, the
     item counter) sits here rather than picking its own small number. */
  --size-min: 0.875rem;   /* 14px — the hard floor */
  /* Price weight is the NAME's weight by definition: a price set lighter than
     the dish reads as an afterthought and slows the scan. One token, both
     rules, so they cannot drift apart. */
  --w-name: 600;

  /* derived — layout rhythm, not colour */
  --pad: 16px;
  --maxw: 680px;
  --navh: 54px;
  --r-sm: min(10px, var(--radius));
  --r-pill: 999px;
  /* One ratio for every photo in a layout — a column of mismatched crops is
     the single most common way a QR menu looks unfinished. Each layout sets
     this once and both the frame and the <img> inside it read it. */
  --ratio: 16 / 10;
}

/* Accent washes. The plain rgba-free fallback is declared first so a browser
   without color-mix() simply gets the line colour instead of nothing.
   --tint and --badge-bg are ALSO emitted by the API (brand-derived), and its
   :root wins over this html rule — which is the intent. */
html {
  --tint: var(--line);
  --tint-strong: var(--line);
  --badge-bg: var(--line);
}
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  html {
    --tint: color-mix(in srgb, var(--accent) 12%, transparent);
    --tint-strong: color-mix(in srgb, var(--accent) 22%, transparent);
    --badge-bg: color-mix(in srgb, var(--accent) 16%, transparent);
  }
}
/* The API's :root is unconditional, so on a browser without color-mix() its
   --tint/--badge-bg compute to the guaranteed-invalid value and every
   `background: var(--tint)` silently drops out. `html:root` outranks the plain
   `:root` it injects, so the flat fallback wins there too. */
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  html:root {
    --tint: var(--line);
    --tint-strong: var(--line);
    --badge-bg: var(--line);
  }
}

/* ------------------------------------------------------------------ base */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
  /* A phone menu never scrolls sideways. `clip` rather than `hidden` on
     purpose: `hidden` would make the body a scroll container, which breaks
     the sticky section nav and window scroll maths. */
  overflow-x: hidden;
  overflow-x: clip;
  overscroll-behavior-y: none;
  -webkit-font-smoothing: antialiased;
  padding-bottom: env(safe-area-inset-bottom);
}

h1, h2, h3 { font-family: var(--heading); font-weight: 600; margin: 0; line-height: 1.35; }
p { margin: 0; }
img { max-width: 100%; display: block; }
a { color: inherit; }

button {
  font: inherit; color: inherit; cursor: pointer;
  border: 0; background: none; -webkit-tap-highlight-color: transparent;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

[hidden] { display: none !important; }

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad); }

/* ----------------------------------------------------------- lang toggle */
/* Pinned rather than in-flow: the diner can switch language from anywhere in
   a long menu. Its height and offset are tuned so that once the section nav
   sticks to the top, the pill sits level with the chips and reads as part of
   that bar instead of colliding with it. */
.lang-toggle {
  position: fixed;
  top: calc(10px + env(safe-area-inset-top));
  inset-inline-end: 12px;
  z-index: 60;
  /* A full tap target in BOTH dimensions — this is the one control a diner
     reaches for while holding a phone one-handed. Its centre now lands level
     with the section chips, which are on the same 44px floor. */
  min-width: var(--tap); min-height: var(--tap); height: var(--tap);
  padding: 0 14px;
  border-radius: var(--r-pill);
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  font-size: var(--size-min); font-weight: 600; letter-spacing: .04em;
}
.lang-toggle:active { transform: scale(.96); }

/* ------------------------------------------------------------- skeleton */
.skeleton { padding-bottom: 40px; }
.sk {
  background: var(--line);
  border-radius: var(--r-sm);
  opacity: .55;
}
.sk-cover { height: 150px; border-radius: 0; }
.sk-id { max-width: var(--maxw); margin-inline: auto; padding: 20px var(--pad) 0; }
.sk-logo { width: 62px; height: 62px; border-radius: var(--r-pill); margin-bottom: 14px; }
.sk-title { height: 26px; width: 62%; margin-bottom: 10px; }
.sk-sub { height: 14px; width: 42%; }
.sk-chips {
  display: flex; gap: 8px;
  max-width: var(--maxw); margin-inline: auto; padding: 24px var(--pad) 0;
}
.sk-chip { height: 34px; width: 84px; border-radius: var(--r-pill); }
.sk-rows {
  display: flex; flex-direction: column; gap: 12px;
  max-width: var(--maxw); margin-inline: auto; padding: 24px var(--pad) 0;
}
.sk-row { height: 64px; border-radius: var(--radius); }

@media (prefers-reduced-motion: no-preference) {
  .sk { animation: skpulse 1.5s ease-in-out infinite; }
  .sk-row:nth-child(2) { animation-delay: .1s; }
  .sk-row:nth-child(3) { animation-delay: .2s; }
  .sk-row:nth-child(4) { animation-delay: .3s; }
  .sk-row:nth-child(5) { animation-delay: .4s; }
}
@keyframes skpulse { 0%, 100% { opacity: .35; } 50% { opacity: .7; } }

/* ------------------------------------------------------- 404 / error state */
.state {
  min-height: 100svh;
  display: grid; place-items: center;
  padding: 40px var(--pad);
  text-align: center;
}
.state-inner { max-width: 34ch; }
.state-mark {
  display: inline-flex; width: 56px; height: 56px;
  align-items: center; justify-content: center;
  margin-bottom: 18px;
  color: var(--brand);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
}
.state-mark svg { width: 26px; height: 26px; }
.state h1 { font-size: var(--size-section); margin-bottom: 10px; }
.state p { color: var(--muted); font-size: var(--size-desc); line-height: var(--lh-desc); white-space: pre-line; }
/* SURFACE TOKENS — the owner's per-page colours, on top of the theme.
   Every one is read with the value this page uses TODAY as its var() fallback,
   so a shop that overrides nothing renders byte-for-byte as it always has.
   m.js keeps only the owner's literal hex out of the API's surface_css block
   for exactly that reason; see the note on surfaceDecls(). */
.state-btn {
  margin-top: 22px; min-height: var(--tap); padding: 0 22px;
  border-radius: var(--r-pill);
  background: var(--btn-bg, var(--brand)); color: var(--btn-text, var(--surface));
  font-size: var(--size-desc); font-weight: 600;
}

/* -------------------------------------------------------------- the hero */
/* --header-bg is the surface control for the identity block. Its fallback is
   `transparent`, which is exactly what this block does today — the page colour
   shows through and nothing moves until an owner picks a colour. */
.hero { background: var(--header-bg, transparent); }
.cover {
  position: relative;
  width: 100%; height: clamp(150px, 34vw, 260px);
  overflow: hidden;
  /* THE OWNER'S BACKDROP. A banner with its background removed floats on this;
     `--cover-bg` is emitted only when they chose one (a 2–3 stop gradient from
     surface_css), so an untouched menu keeps the tint it has today. */
  background: var(--cover-bg, var(--tint));
}
.cover-img { width: 100%; height: 100%; object-fit: cover; }
/* A scrim on the bottom edge only, so the identity block below never fights
   the photograph for attention. */
.cover::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 45%;
  background: linear-gradient(to top, var(--bg), transparent);
  pointer-events: none;
}
/* A custom header colour has to take the scrim with it, or the photo fades
   into the page colour over a header that is no longer that colour. m.js sets
   this attribute only when the owner actually picked one, so the default path
   above is untouched. */
html[data-header-set] .cover::after {
  background: linear-gradient(to top, var(--header-bg), transparent);
}

.ident {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 26px var(--pad) 18px;
  text-align: center;
}
.has-cover .ident { padding-top: 0; margin-top: -26px; position: relative; z-index: 1; }

/* No cover? A quiet brand rule stands in for the photograph, so the header
   still reads as designed rather than as something missing. */
.hero:not(.has-cover) .ident { padding-top: calc(26px + env(safe-area-inset-top)); }
.hero:not(.has-cover)::before {
  content: ""; display: block; height: 3px;
  background: linear-gradient(to left, var(--brand), var(--accent));
}

.logo {
  width: 76px; height: 76px; margin: 0 auto 14px;
  /* THE OWNER'S OWN BADGE, the same three tokens the dish photo has (founder,
     2026-07-25: "the shape for the logo and the border of it to have the same
     as the menu item"). Each is emitted only when they chose it, so a shop that
     has restyled nothing still gets the pill, the surface and the hairline it
     draws today. `transparent` is a value they can pick for the fill. */
  border-radius: var(--logo-radius, var(--r-pill));
  background: var(--logo-bg, var(--surface));
  border: 1px solid var(--logo-line, var(--line));
  box-shadow: var(--shadow);
  padding: 8px;
  display: grid; place-items: center;
  overflow: hidden;
}
.logo-img { width: 100%; height: 100%; object-fit: contain; }

.shop-name {
  /* The one place the ramp is deliberately exceeded — a shop's own name is the
     page's title, so it scales with the viewport and with the theme, and never
     drops under the section size. */
  font-size: clamp(var(--size-section), 6.5vw, calc(var(--size-section) * 1.4));
  font-weight: 700;
  letter-spacing: var(--tracking);
  overflow-wrap: anywhere;
}
.tagline {
  margin-top: 8px;
  color: var(--muted);
  font-size: var(--size-desc);
  line-height: var(--lh-desc);
  overflow-wrap: anywhere;
}
.tagline:empty { display: none; }
/* «التقييمات» (2026-09-28) — the average under the tagline, drawn only when sent. */
.rating { display: inline-flex; align-items: center; gap: 2px; margin: 6px 0 0; font-size: 14px;
  font-variant-numeric: tabular-nums; }
.rating[hidden] { display: none; }
.rating svg { width: 16px; height: 16px; }
.rating svg.on { fill: #F5A623; }
.rating svg.off { fill: currentColor; opacity: 0.25; }

/* Phone, socials and the map link. A shop can list a dozen accounts, so these
   are icon-only circles that wrap cleanly at 360px — each one names its
   platform through aria-label/title rather than a visible caption. */
.contact {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  margin-top: 18px;
}
.contact:empty { display: none; }
.contact a {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); flex: none; padding: 0;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--text);
  text-decoration: none;
}
.contact a:active { background: var(--tint); border-color: var(--accent); }
@media (hover: hover) {
  .contact a:hover { background: var(--tint); border-color: var(--accent); }
}
.contact svg { width: 19px; height: 19px; flex: none; color: var(--brand); }

/* ------------------------------------------------------------------ hours
   WHEN the shop is open — and nothing at all about what it serves. A dessert
   shop is open at 9am and does no breakfast, so there is no meal wording, no
   meal icon and no meal inference anywhere in this block.

   THREE STATES, never two. `is-open` and `is-closed` are answers; `is-unknown`
   is the absence of one and reads "hours not listed". They are told apart by
   the WORD first and by the mark second, so the distinction survives any theme
   and any owner CSS that flattens colour. */
.hours {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 4px var(--pad) 0;
}
html[data-layout="grid"] .hours { max-width: 1040px; }
.hours-h {
  font-size: var(--size-min); font-weight: 600;
  color: var(--muted); letter-spacing: var(--tracking);
  margin-bottom: 6px;
}
.hours-now {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  font-size: var(--size-desc); line-height: var(--lh-desc);
}
.hours-state { font-weight: 700; }
.hours-today { display: inline-flex; flex-wrap: wrap; gap: 8px; }
.hr-rg { display: inline-flex; align-items: baseline; gap: 5px;
         flex-wrap: wrap; }
.hr-clock { font-variant-numeric: tabular-nums; white-space: nowrap; }
.hr-next, .hr-none { color: var(--muted); }
/* A filled mark for open, a hollow one for closed, a dashed ring for "we were
   not told". The third is a real state and must not look like the second. */
.hr-dot {
  width: 10px; height: 10px; flex: none;
  border-radius: 50%;
  border: 1.5px solid var(--muted);
  background: transparent;
}
.hours-now.is-open .hr-dot { background: var(--accent); border-color: var(--accent); }
.hours-now.is-unknown .hr-dot { border-style: dashed; }

/* The period that is overriding today — Ramadan, Eid, a renovation. Named, so
   a diner who can see the printed sign on the door understands the mismatch. */
.hours-special {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
  margin-top: 6px;
  font-size: var(--size-min); color: var(--muted);
}
.hours-sp-name { font-weight: 600; color: var(--text); }
.hours-sp-dates { font-variant-numeric: tabular-nums; }

/* The full week, folded away. A menu is what the diner came for; the week is
   what they open when today's line is not the answer they needed. */
.hours-week { margin-top: 8px; }
.hours-week-sum {
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  font-size: var(--size-min); font-weight: 600;
  color: var(--brand);
  cursor: pointer;
}
.hours-week-list {
  display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 4px 12px; margin-bottom: 8px;
}
.hr-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; padding: 7px 0;
  font-size: var(--size-desc); line-height: var(--lh-desc);
  border-bottom: 1px solid var(--line);
}
.hr-row:last-child { border-bottom: 0; }
.hr-row.on { font-weight: 600; }
.hr-day-name { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; }
.hr-today-tag {
  font-size: var(--size-min); font-weight: 700;
  color: var(--brand);
}
.hr-day-val { display: inline-flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; text-align: end; }
.hr-week-note {
  margin: 8px 0 4px;
  font-size: var(--size-min); line-height: var(--lh-desc); color: var(--muted);
}

/* -------------------------------------------------------- section nav */
.secnav {
  position: sticky; top: 0; z-index: 40;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
@supports (backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px)) {
  .secnav {
    background: var(--bg);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
  }
}
.secnav-track {
  display: flex; gap: 8px;
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 10px var(--pad);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
}
.secnav-track::-webkit-scrollbar { display: none; }
/* Keeps the last chip clear of the pinned language pill. A flex spacer is
   used rather than padding-inline-end, which several browsers drop at the
   far end of a scroll container. Sized off --tap, because the pill it is
   clearing is now a full tap target and the two must move together. */
.secnav-track::after { content: ""; flex: none; width: calc(var(--tap) + 20px); }

/* …and chips passing that corner fade out rather than sliding under the pill.
   The gradient runs toward the inline-end, which flips with the direction. */
html[dir="rtl"] .secnav-track {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - var(--tap) - 26px), transparent);
  mask-image: linear-gradient(to left, #000 calc(100% - var(--tap) - 26px), transparent);
}
html[dir="ltr"] .secnav-track {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - var(--tap) - 26px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - var(--tap) - 26px), transparent);
}

/* A chip is a navigation control, so it carries the full tap target rather
   than the 34px pill height that merely looked right. */
.secchip {
  flex: none;
  min-height: var(--tap); min-width: var(--tap); padding: 0 16px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--chip-bg, var(--surface));
  color: var(--chip-text, var(--muted));
  font-size: var(--size-min); font-weight: 500;
  letter-spacing: var(--tracking);
  white-space: nowrap;
  transition: background .18s ease, color .18s ease, border-color .18s ease;
}
/* The ACTIVE chip is a button, not a chip — it takes the button tokens, whose
   fallbacks are the brand and surface it has always used. */
.secchip.on {
  background: var(--btn-bg, var(--brand));
  border-color: var(--btn-bg, var(--brand));
  color: var(--btn-text, var(--surface));
  font-weight: 600;
}

/* ---------------------------------------------------------- the sections */
.menu {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 8px var(--pad) 24px;
}
.menu.wide { max-width: 1040px; }
html[data-layout="grid"] .secnav-track { max-width: 1040px; }

.section { scroll-margin-top: calc(var(--navh) + 8px); padding-top: 26px; }
.section:first-child { padding-top: 20px; }

.section-head {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 16px;
}
.section-head h2 {
  font-size: var(--size-section); font-weight: 700;
  letter-spacing: var(--tracking);
  color: var(--heading-color, var(--brand));
  overflow-wrap: anywhere;
}
.section-head .rule {
  flex: 1 1 auto; height: 1px; background: var(--line); min-width: 16px;
}
.section-head .count {
  flex: none; font-size: var(--size-min); color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* SECTION SEPARATION — the theme's `section_rule` switch.

   The public payload does NOT carry it (it is only on GET /owner/themes), so
   m.js mirrors the server's table and stamps data-section-rule / data-heading-
   case on <html>. An unknown theme simply gets no attribute and falls through
   to the default treatment above, which is the "line" case. */
html[data-section-rule="tint"] .section-head {
  background: var(--tint);
  border-radius: var(--r-sm);
  padding: 10px 12px;
  margin-inline: -12px;
}
html[data-section-rule="tint"] .section-head .rule { background: transparent; }

html[data-section-rule="none"] .section-head .rule { display: none; }

/* Uppercase is a DISPLAY choice and applies to Latin only — Arabic has no
   case, so this is a no-op on an Arabic heading rather than damage. The extra
   tracking is what keeps an all-caps line from setting too tight. */
html[data-heading-case="upper"] .section-head h2,
html[data-heading-case="upper"] .shop-name {
  text-transform: uppercase;
  letter-spacing: max(var(--tracking), .03em);
}

/* ---------------------------------------------------------------- offers
   The deals lead the menu, and they should read as a deliberate, special SET —
   not another section that happens to wear a tinted heading. So the whole
   offers section sits inside a calm accent-wash card: the --tint every palette
   derives, a soft --tint-strong hairline, and the theme's own --radius, with
   the discounted dishes grouped visibly on it. The heading is marked out with a
   short brand→accent bar on its leading edge rather than a band of its own —
   drawn, so there is no emoji and nothing to load. Every value is a theme
   token, so the treatment restyles with the palette and reads correctly in RTL;
   the item cards inside are untouched — their struck price and saving pill
   already render. The selectors carry `.section` too, so this out-ranks the
   `data-section-rule="tint"` heading rule on the palettes that set it. */
.section.section-offers {
  background: var(--tint);
  border: 1px solid var(--tint-strong);
  border-radius: var(--radius);
  padding: 16px 14px;
}
.section.section-offers .section-head {
  background: transparent;
  padding: 0;
  margin-inline: 0;
  margin-bottom: 14px;
}
.section.section-offers .section-name {
  display: inline-flex; align-items: center; gap: 9px;
}
.section.section-offers .section-name::before {
  content: ""; flex: none;
  width: 4px; height: 1.05em;
  border-radius: var(--r-pill);
  background: linear-gradient(var(--brand), var(--accent));
}
/* The rule stays as a quiet accent divider (it also flex-grows, keeping the
   count pinned to the far edge) instead of the old transparent placeholder. */
.section.section-offers .section-head .rule { background: var(--tint-strong); }
.section.section-offers .section-head .count { font-weight: 600; color: var(--brand); }

.items { display: flex; flex-direction: column; gap: 12px; }

/* ------------------------------------------------------------ item parts */
.item { position: relative; min-width: 0; }

/* Owner text carries dir="auto" so a mixed Arabic/Latin string is ORDERED
   correctly — that is what keeps a full stop from jumping to the wrong end.
   Alignment, though, stays pinned to the page direction: letting each line
   align to its own script leaves a card ragged when an Arabic dish sits above
   its English name. */
html[dir="rtl"] .item-name, html[dir="rtl"] .item-name-alt,
html[dir="rtl"] .item-desc, html[dir="rtl"] .section-head h2,
html[dir="rtl"] .tag { text-align: right; }
html[dir="ltr"] .item-name, html[dir="ltr"] .item-name-alt,
html[dir="ltr"] .item-desc, html[dir="ltr"] .section-head h2,
html[dir="ltr"] .tag { text-align: left; }

.item-head {
  display: flex; align-items: baseline; gap: 10px;
  min-width: 0;
}
/* The name and its badges, together. INLINE FLOW, not a flex column: the badges
   run on from the name like words and wrap the same way — beside a short name,
   trailing onto the next line after a long one — which is what "next to the
   name" actually means. The price stays put at the end of .item-head. The
   second-language name is a block, so it always drops to its own line under the
   name and whatever badges rode alongside it. */
/* Hug the content (name + badges), do NOT grow: growing made it split the
   free space with the leader, so the pill never had room to sit inline and
   dropped to its own line. At content width the name and its badges share one
   line, the leader fills what is left, and the price stays at the end. */
.item-lead {
  flex: 0 1 auto; min-width: 0;
}
.item-name {
  display: inline;
  font-family: var(--heading);
  font-size: var(--size-item); font-weight: var(--w-name);
  overflow-wrap: anywhere;
}
/* The second-language name. Quieter than the dish it belongs to, by weight and
   colour — NOT by dropping under the legibility floor, which is where it used
   to sit at 12.5px. Its own line under the name and badges. */
.item-name-alt {
  display: block;
  font-family: var(--font);
  font-size: var(--size-desc); font-weight: 400; color: var(--muted);
  line-height: var(--lh-desc);
  letter-spacing: .01em; margin-top: 2px;
}

/* THE BADGE, BESIDE THE NAME. One uniform pill for every declared badge
   (الشارات), theme-driven so the single rule reads correctly on all twelve
   palettes: a soft accent-tinted fill, brand ink, an accent hairline. Never
   the old loud/quiet two-tier — the founder wants them to read as one set.
   inline-flex + vertical-align keeps each pill on the name's line, and the
   inline margins give the run of them air without a flex gap. */
.item-badge {
  display: inline-flex; align-items: center; gap: 4px;
  vertical-align: middle;
  margin-inline-start: 7px; margin-block: 2px;
  font-size: var(--size-min); font-weight: 600; line-height: 1;
  padding: 3px 8px; border-radius: var(--r-pill);
  background: var(--badge-bg);
  color: var(--badge-text, var(--brand));
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  white-space: nowrap;
}
.item-badge svg {
  width: 12px; height: 12px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
/* A palette that asked for outline badges (linen) keeps its lighter touch. */
html[data-badge="outline"] .item-badge { background: transparent; }
/* Same weight as .item-name, from the same token: the price is half of what a
   diner is deciding on, and setting it lighter buries it. */
.item-price {
  flex: none;
  /* Pushed to the end. In the list layout the leader already fills the gap so
     this resolves to zero; in grid/card (no leader) it is what keeps the price
     at the far edge now that .item-lead hugs its content instead of growing. */
  margin-inline-start: auto;
  font-size: var(--size-price); font-weight: var(--w-name);
  color: var(--price-color, var(--brand));
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.item-price .cur {
  font-size: var(--size-min); font-weight: 500;
  opacity: .75; margin-inline-start: 3px;
}

/* ------------------------------------------------------------- a discount
   The old price sits BESIDE the current one, struck through, and the saving is
   printed under the pair. m.js computes that percentage from these two numbers
   at render time — there is no typed discount anywhere in the path — so the
   claim and the prices can never drift apart.

   The block keeps `flex: none` like the plain price it replaces, so every
   layout below places it exactly where a single price used to sit. */
.price-wrap {
  flex: none;
  margin-inline-start: auto;   /* end-aligned like .item-price, for the no-leader layouts */
  display: flex; flex-direction: column; align-items: flex-end; gap: 3px;
  min-width: 0;
}
.price-line { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
/* Muted and a step down, but never under the legibility floor: it is still a
   number a diner reads, it just is not the one being charged. */
.item-price-old {
  font-size: max(var(--size-min), calc(var(--size-price) * .82));
  font-weight: 500;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-decoration-thickness: from-font;
}
.item-price-old .cur { font-size: var(--size-min); font-weight: 500; opacity: .7; margin-inline-start: 3px; }
.item-save {
  font-size: var(--size-min); font-weight: 700;
  line-height: 1.4;
  padding: 2px 9px;
  border-radius: var(--r-pill);
  background: var(--badge-bg);
  border: 1px solid var(--accent);
  color: var(--text);
  white-space: nowrap;
  letter-spacing: var(--tracking);
}
html[data-badge="outline"] .item-save {
  background: transparent; color: var(--brand);
}

/* ------------------------------------------------------------- a combo
   What is inside it, directly under the name and above the description: for a
   meal deal the contents ARE the product, where the description is a garnish.
   Compact by design — this sits inside a phone card. */
.includes {
  list-style: none;
  margin: 7px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px;
}
.includes li {
  position: relative;
  padding-inline-start: 14px;
  font-size: var(--size-desc); line-height: var(--lh-desc);
  color: var(--text);
  overflow-wrap: anywhere;
}
/* A drawn dot rather than a list marker, so it inherits the accent and sits on
   the correct side in both directions without a marker override. */
.includes li::before {
  content: ""; position: absolute;
  inset-inline-start: 3px; top: calc(var(--lh-desc) * .5em - 2px);
  width: 4px; height: 4px; border-radius: 50%;
  background: var(--accent);
}

.item-desc {
  margin-top: 5px;
  color: var(--muted);
  font-size: var(--size-desc); line-height: var(--lh-desc);
  overflow-wrap: anywhere;
}

/* ------------------------------------------- calories, the walk, allergens
   Health information under a dish. It is styled to be FOUND, not to be sold:
   muted, at the small-text floor, no accent fill, no ranking treatment. Nothing
   here may look like a recommendation, a warning, or a score — a calorie count
   is a fact about the plate, and an allergen mark is the owner's declaration,
   and neither is a judgement of the diner reading it. */
.item-facts {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 4px 10px; margin-top: 7px;
}
.item-kcal {
  font-size: var(--size-min); font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* THE ASSUMED BODY IS ON THE ELEMENT ITSELF (m.js writes the whole sentence
   into both `title` and `aria-label`), so the minutes can never be styled,
   truncated or translated away from the body they were computed for — the
   accessible name is a single string and there is no rule here that can reach
   inside it. Deliberately the same muted weight as everything else in this
   row: the walk is not a call to action, and the little figure must not turn
   it into one. */
.item-walk {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--size-min); font-weight: 400; color: var(--muted);
  line-height: var(--lh-desc); white-space: nowrap;
}
/* Sized off the text, not in px, so it stays the same optical weight as the
   number beside it at every step of the type ramp. */
.item-walk .walk-ic { display: inline-flex; flex: none; }
.item-walk .walk-ic svg { width: 1.15em; height: 1.15em; display: block; stroke-width: 1.7; }
.item-walk .walk-n { font-variant-numeric: tabular-nums; }

/* WHEN A DISH IS SERVED, as its owner declared it. Quiet on purpose: this is
   the same class of thing as the calorie count beside it — a fact about the
   plate — and never a badge, a recommendation or a restriction. A dish with no
   mealtime declared renders nothing at all here (untagged means all day), so
   there is no "any time" state to style. */
.item-meals {
  display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px;
  font-size: var(--size-min); line-height: var(--lh-desc); color: var(--muted);
}
.meals-lead { font-weight: 700; white-space: nowrap; }
.item-meals .meal {
  font-weight: 500; white-space: nowrap;
  padding: 1px 7px; border-radius: var(--r-pill);
  border: 1px solid var(--line);
}

/* A dish's declared allergens. Rendered ONLY when the owner declared at least
   one — m.js emits nothing for an empty list, so there is no "none" state here
   to style, on purpose: an absent mark is an absence of information, and a
   stylesheet must never be able to turn that into a clean bill of health. */
.allergens {
  flex-basis: 100%;
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
}
.alg-lead {
  font-size: var(--size-min); font-weight: 700; color: var(--muted);
  white-space: nowrap;
}
.alg {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--size-min); font-weight: 500; line-height: var(--lh-desc);
  padding: 2px 9px;
  border-radius: var(--r-pill);
  /* Hairline and neutral. NOT the accent pill a badge or a saving wears: those
     mark something good, and this marks something true. */
  border: 1px solid var(--line);
  color: var(--muted);
  background: transparent;
}
.alg svg { width: 16px; height: 16px; flex: none; }
.alg-name { overflow-wrap: anywhere; }

/* The caveat, printed verbatim from the API above the first section — the
   business declared this, and an allergen it does not list has not been ruled
   out. Full-width and at the legibility floor like every other sentence on the
   page; quiet, but never smaller than the page's smallest readable text. */
/* THE ALLERGEN CAVEAT — now at the FOOT of the menu, led by an info mark.
   Quiet by design: it is small print for most readers, and the person who
   needs it arrives via the mark on a chip group (.alg-info) rather than by
   having it shouted at everyone on the way in. */
.allergen-note {
  margin: calc(var(--pad) * 1.25) 0 var(--pad);
  font-size: var(--size-min);
  line-height: var(--lh-desc);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  overflow-wrap: anywhere;
  /* Arriving here from a chip must not tuck it under the sticky nav. */
  scroll-margin-top: calc(var(--navh, 0px) + 16px);
}
/* The closed state: an icon and four words. The disclaimer itself is one tap
   away for the reader who wants it, and out of everyone else's way. */
.alg-note-sum {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 14px;
  min-height: var(--tap);
  cursor: pointer;
  list-style: none;
  color: var(--text);
  font-weight: 600;
}
.alg-note-sum::-webkit-details-marker { display: none; }
.alg-note-sum:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.allergen-note[open] .alg-note-sum { padding-bottom: 4px; }
.alg-note-mark {
  flex: none;
  display: inline-flex;
  width: 18px;
  height: 18px;
  color: var(--muted);
}
.alg-note-mark svg { width: 100%; height: 100%; }
.alg-note-body { margin: 0; padding: 0 14px 13px; }
/* The mark on a chip group that opens the note. A real anchor, so it works
   before any script runs. */
.alg-info {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  min-width: 24px;
  color: var(--muted);
  border-radius: 50%;
  text-decoration: none;
}
.alg-info svg { width: 15px; height: 15px; }
.alg-info:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.allergen-note:target { border-color: var(--brand); }
.allergen-note b {
  display: block;
  margin-bottom: 2px;
  color: var(--text);
  font-weight: 600;
}
.alg-note-mark {
  flex: none;
  display: inline-flex;
  width: 18px;
  height: 18px;
  margin-top: 1px;
  color: var(--muted);
}
.alg-note-mark svg { width: 100%; height: 100%; }
.alg-note-body { min-width: 0; }
/* The mark on a chip group that jumps to the note. A real tap target. */
.alg-info {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  min-width: 24px;
  color: var(--muted);
  border-radius: 50%;
  text-decoration: none;
}
.alg-info svg { width: 15px; height: 15px; }
.alg-info:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The caveat is briefly marked when jumped to, so the eye lands on it. */
.allergen-note:target { border-color: var(--brand); }

/* ------------------------------------------------------------- variants
   A dish's ways to have it, in the same quiet register as the calories and the
   allergens above: a fact about the plate, never a badge or a claim. m.js emits
   ONE of two shapes — a priced list when the variants carry their own prices
   (the head then shows the range they span), or same-price choice chips when
   they do not (the single item price already covers them). Both take the full
   width of the facts row so they read as their own quiet block, and every colour
   resolves through the theme's tokens like the rest of this region. */
.item-variants {
  flex-basis: 100%;
  list-style: none; margin: 3px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.variant-row {
  display: flex; align-items: center;
  gap: 8px;
  font-size: var(--size-desc); line-height: var(--lh-desc);
  color: var(--text);
}
.variant-name { min-width: 0; overflow-wrap: anywhere; }
/* The variant's own figure, on the same price-colour token as every other price
   on the page — a step down in size from the headline, never below the floor. */
.variant-price {
  flex: none;
  /* The price holds the END of the row; the name and its tag sit together
     at the start — three children under space-between put the tag in the
     MIDDLE of the row, which is where the founder found it. */
  margin-inline-start: auto;
  font-size: var(--size-min); font-weight: 600;
  color: var(--price-color, var(--brand));
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.variant-price .cur {
  font-size: var(--size-min); font-weight: 500;
  opacity: .75; margin-inline-start: 3px;
}

/* SAME-PRICE CHOICES — «بارد · حار». Pills of the tag family, but deliberately
   quieter than a tag or a badge: those mark something the shop is claiming,
   where a choice just names an option the diner can ask for. A neutral tint and
   muted ink say "pick one" without shouting, and set them apart from both the
   brand-inked tag and the accent-filled badge. */
.item-choices {
  flex-basis: 100%;
  display: flex; flex-wrap: wrap; gap: 6px;
}
.choice {
  font-size: var(--size-min); font-weight: 500;
  line-height: var(--lh-desc);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--tint);
  color: var(--muted);
  border: 1px solid var(--line);
  overflow-wrap: anywhere;
}

.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.tag {
  font-size: var(--size-min); font-weight: 500;
  line-height: var(--lh-desc);
  padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--tint);
  color: var(--brand);
  border: 1px solid var(--tint-strong);
  overflow-wrap: anywhere;
}

/* THE BADGE — a "signature"/"most ordered"/"chef's pick" tag, marked out from
   the owner's other tags.

   Published menu-engineering work puts the lift from highlighting at 13-20%,
   but only while the highlight stays SCARCE: a category where everything is
   starred says nothing. m.js therefore promotes at most three per section and
   renders the rest as ordinary tags — the owner's word is never dropped, it
   just stops shouting after the third one. */
.tag.badge {
  background: var(--badge-bg);
  border-color: var(--accent);
  color: var(--text);
  font-weight: 700;
  letter-spacing: var(--tracking);
}
/* A theme may ask for the outline treatment instead of the filled one (linen
   sets badge:"outline"), for palettes where a filled pill is too loud. */
html[data-badge="outline"] .tag.badge {
  background: transparent;
  border-color: var(--accent);
  color: var(--brand);
}

/* A DECLARED badge — one of the fourteen the shop picked off a list, rather
   than a word this file guessed at. It carries its mark, and it takes the same
   two treatments everything else in this row does: `loud` keys (signature, most
   ordered, new, chef's choice) also get `.badge` above and shout; the other ten
   stay ordinary tags. No third style: a diner reading a menu should not have to
   learn a vocabulary of pill shapes. */
.tag-key { display: inline-flex; align-items: center; gap: 5px; }
.tag-key svg { width: 14px; height: 14px; flex: none; }

/* The photo button. Present only when the item actually has an image — there
   is no placeholder tile anywhere in this stylesheet, by design. */
/* CONSISTENT CROP. The frame owns the ratio — one --ratio per layout, set on
   the layout container — so every photo in a menu is cropped identically no
   matter what the owner uploaded. A column of mismatched crops is the fastest
   way a real menu starts looking like a draft.

   Because the frame's height is then definite, `height: 100%` on the image
   resolves everywhere and the image no longer has to restate the ratio. */
.shot {
  position: relative;
  display: block; padding: 0; overflow: hidden;
  /* THE OWNER'S OWN FRAME. `--shot-bg` is emitted only when they chose one
     (see surface_css), so an untouched menu still gets `--tint` and looks
     exactly as it did. `transparent` is a value they can pick, which is the
     founder's "or to be transparent". */
  background: var(--shot-bg, var(--tint));
  /* THE OWNER'S OWN SHAPE. `--shot-radius` is emitted only when they chose one
     (surface_css), so an untouched menu keeps the theme's corner — which for a
     square-cornered theme like "contrast" is a knife edge, and is exactly what
     the founder was looking at. A circle is `50%`, so it needs the box to be
     square: the frame already owns the ratio, so it is. */
  border-radius: var(--shot-radius, var(--r-sm));
  flex: none;
  aspect-ratio: var(--ratio);
  /* It is a button that opens the photo under the name, so it is also a
     tap target. */
  min-width: var(--tap); min-height: var(--tap);
}
.shot img { width: 100%; height: 100%; object-fit: cover; }
.shot:active { opacity: .88; }

/* PER-THEME FRAME. The photo takes its corner from --radius (through --r-sm)
   and a hairline ring from --line, so a sharp-cornered palette and a soft one
   frame their food differently — the frame reads the theme the owner chose.
   An overlay pseudo-element rather than a border or inset shadow on the button:
   the cover image fills the box and would paint over anything drawn behind it,
   where this ring sits ABOVE the photo's edge and follows the very same crop.
   It changes no geometry, so the consistent crop is untouched in all four
   layouts. Theme tokens only. */
.shot::after {
  content: ""; position: absolute; inset: 0;
  border: 1px solid var(--shot-line, var(--line));
  border-radius: inherit;
  pointer-events: none;
}

/* THE CUT-OUT — the background-removed dish. A transparent PNG of the food with
   no background must NOT sit in the framed, cropped photo box; it floats on the
   card instead. The tint frame and the hard crop come off, the ring is dropped
   (a rectangle around a floating silhouette makes no sense), the dish is
   contained so it is never cut, and a soft drop-shadow — traced from the PNG's
   own alpha, so it hugs the food's outline — lifts it off the surface. The
   shadow colour is mixed from --text so it tracks the palette and simply drops
   out where color-mix() is unsupported, leaving the image itself intact. */
.shot.is-cut {
  background: transparent;
  /* The shadow is drawn OUTSIDE the box, so it must not be clipped. */
  overflow: visible;
}
.shot.is-cut::after { display: none; }
.shot.is-cut img {
  object-fit: contain;
  /* BREATHING ROOM VIA scale(), NEVER percentage padding.
     `padding: 5%` here resolved against the CONTAINING BLOCK'S width — that is
     what a percentage padding does, on every side, including top and bottom —
     not against the 63px thumbnail. In a 632px-wide item row that is 31.4px of
     padding each side on a 63px box: a content box of exactly ZERO, an image
     loaded and then given no size, and a thumbnail that renders EMPTY. It
     survived a narrow phone column (where 5% is small) and vanished on a
     desktop, which is precisely how the founder found it — Chrome and Safari
     both, at full width.
     `scale()` is relative to the element's OWN box, so it means the same thing
     at every size and cannot collapse the image. It also costs no layout. */
  transform: scale(.92);
  filter: drop-shadow(0 5px 9px color-mix(in srgb, var(--text) 26%, transparent));
}

/* =========================================================== LAYOUT: photo
   The image leads and the food is the hero. Items without a photo become
   text cards of the same family, marked by a brand edge, so a half-shot
   menu still reads as one deliberate design. */
.layout-photo { --ratio: 16 / 10; }
.layout-photo .items { gap: 16px; }
.layout-photo .item {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.layout-photo .shot { width: 100%; border-radius: 0; }
.layout-photo .item-body { padding: 14px 16px 16px; }

.layout-photo .item.no-media {
  border-inline-start: 3px solid var(--accent);
}
.layout-photo .item.no-media .item-body { padding: 16px; }

/* ============================================================ LAYOUT: list
   Name .......... price. The printed menu. Hairlines, no boxes. */
.layout-list { --ratio: 1 / 1; }
.layout-list .items { gap: 0; }
.layout-list .item {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 15px 2px;
  border-bottom: 1px solid var(--line);
}
.layout-list .item:last-child { border-bottom: 0; }
.layout-list .shot { width: 54px; margin-top: 2px; }
.layout-list .item-body { flex: 1 1 auto; min-width: 0; }

/* The leader rule that carries the eye from the dish to its price. */
.layout-list .item-head { align-items: baseline; gap: 6px; }
/* The printed-menu layout puts name, leader and price on one baseline. A price
   block with a saving under it is two lines, so it aligns from the top and lets
   the leader meet the price line itself rather than the pill beneath it. */
.layout-list .item-head:has(.price-wrap) { align-items: flex-start; }
.layout-list .price-wrap { padding-top: 1px; }
.layout-list .leader {
  flex: 1 1 auto;
  min-width: 14px;
  height: 0;
  border-bottom: 1px dotted var(--line);
  transform: translateY(-4px);
}

/* ============================================================ LAYOUT: card
   Each item boxed, image kept small so text-only items sit level with it. */
.layout-card { --ratio: 1 / 1; }
.layout-card .items { gap: 10px; }
.layout-card .item {
  display: flex; align-items: flex-start; gap: 13px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 14px;
}
.layout-card .shot { width: 78px; }
.layout-card .item-body { flex: 1 1 auto; min-width: 0; }

/* ============================================================ LAYOUT: grid
   Two-up tiles on a phone, more columns as the screen allows. */
.layout-grid { --ratio: 1 / 1; }
.layout-grid .items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  /* Each tile keeps its natural height. Stretching would blow a text-only
     tile up to match the photographed one beside it. */
  align-items: start;
}
.layout-grid .item {
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.layout-grid .shot { width: 100%; border-radius: 0; }
.layout-grid .item-body {
  flex: 1 1 auto;
  display: flex; flex-direction: column;
  padding: 12px;
}
.layout-grid .item-head { flex-direction: column; align-items: stretch; gap: 6px; }
/* A two-up tile is narrower than a full-width row, so the grid tightens the
   ramp a little — off the tokens and clamped at the floor, never below it.
   Name and price shrink by the same hair so their weights still read as a
   matched pair. */
.layout-grid .item-name { font-size: max(var(--size-min), calc(var(--size-item) * .9)); }
.layout-grid .item-price {
  align-self: flex-start;
  font-size: max(var(--size-min), calc(var(--size-price) * .94));
}
.layout-grid .item-desc {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* A discounted price in a two-up tile: the head is a column here, so the block
   starts at the inline edge like the single price it replaces. */
.layout-grid .price-wrap { align-self: flex-start; align-items: flex-start; }
.layout-grid .price-line { justify-content: flex-start; }
/* A text-only tile would otherwise sit squat next to a tile with a photo. */
.layout-grid .item.no-media {
  border-top: 3px solid var(--accent);
  min-height: 118px;
}
.layout-grid .item.no-media .item-body { justify-content: center; }
.layout-grid .tags { margin-top: auto; padding-top: 9px; }

@media (min-width: 560px) {
  .layout-grid .items { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
  .layout-grid .items { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------------------------------------------- the "no items" empty state */
.empty {
  text-align: center; color: var(--muted);
  padding: 48px var(--pad);
  font-size: var(--size-desc); line-height: var(--lh-desc);
}
.empty h2 { font-size: var(--size-section); color: var(--brand); margin-bottom: 10px; }


/* -------------------------------------------------------------- branches
   Where the shop IS — one block per address, under the menu. m.js renders this
   only for TWO OR MORE locations; a single-shop page never reaches these
   rules, so there is no "our locations" heading over one address anywhere. */
.branches {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 8px var(--pad) 4px;
}
html[data-layout="grid"] .branches { max-width: 1040px; }
.branches-h {
  font-size: var(--size-section); font-weight: 700;
  letter-spacing: var(--tracking);
  color: var(--brand);
  margin-bottom: 14px;
}
.branches-list { display: flex; flex-direction: column; gap: 10px; }
.branch {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
}
.branch-name {
  font-family: var(--heading);
  font-size: var(--size-item); font-weight: var(--w-name);
  min-width: 0; overflow-wrap: anywhere;
}
.branch-acts { display: flex; gap: 8px; flex-wrap: wrap; }
/* Labelled here, unlike the icon-only contact row: this list can run to six
   shops and "which button is the map" must not be a guess per row. */
.branch-act {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: var(--tap); padding: 0 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--text);
  font-size: var(--size-min); font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
.branch-act svg { width: 17px; height: 17px; flex: none; color: var(--brand); }
.branch-act:active { background: var(--tint); border-color: var(--accent); }
@media (hover: hover) {
  .branch-act:hover { background: var(--tint); border-color: var(--accent); }
}

/* ---------------------------------------------------------------- footer */
.foot {
  padding: 26px var(--pad) calc(34px + env(safe-area-inset-bottom));
  text-align: center;
  border-top: 1px solid var(--line);
  margin-top: 20px;
}
/* A link, therefore a tap target — inline-flex with the floor applied, rather
   than the 12px text and 6px padding it used to be. */
.via {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); min-width: var(--tap);
  color: var(--muted);
  font-size: var(--size-min); letter-spacing: .04em;
  text-decoration: none;
  opacity: .75;
  padding: 6px 14px;
}
.via:active { opacity: 1; }

/* -------------------------------------------------------------- motion */
@media (prefers-reduced-motion: no-preference) {
  .item, .section-head {
    opacity: 0;
    animation: rise .45s cubic-bezier(.22, .8, .3, 1) forwards;
    animation-delay: var(--d, 0ms);
  }
  .hero, .secnav { animation: fade .5s ease both; }
}
@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .secnav-track { scroll-behavior: auto; }
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* --------------------------------------------------------- wider screens */
@media (min-width: 760px) {
  html { --pad: 24px; }
  body { font-size: 17px; }
  /* --tap already floors the button itself; only the glyph inside it grows. */
  .contact svg { width: 20px; height: 20px; }
}

/* -------------------------------------------------- meal serving times
   Deliberately quieter than the opening hours: this answers "why is the
   breakfast not on the page right now", which is a footnote to the menu, not
   the headline the way OPEN/CLOSED is. It borrows the hours block's measure
   and tabular clock so the two read as one family. Every colour resolves
   through the theme's tokens, so a shop that overrode its surfaces keeps its
   own palette here too. */
.mealtimes {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 10px var(--pad) 0;
}
html[data-layout="grid"] .mealtimes { max-width: 1040px; }
.ms-h {
  font-size: var(--size-min); font-weight: 600;
  color: var(--muted); letter-spacing: var(--tracking);
  margin-bottom: 6px;
}
.ms-list { display: flex; flex-direction: column; gap: 4px; }
.ms-row {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px;
  font-size: var(--size-desc); line-height: var(--lh-desc);
  color: var(--muted, inherit);
}
/* The meal being served RIGHT NOW is the one line a diner is looking for, so
   it is the only one at full text colour. */
.ms-row.is-live { color: var(--text); }
.ms-name { font-weight: 600; }
.ms-clock { font-variant-numeric: tabular-nums; white-space: nowrap; }
.ms-now {
  font-size: var(--size-min); font-weight: 700;
  color: var(--chip-text, var(--text));
  background: var(--chip-bg, var(--tint));
  border-radius: 999px; padding: 1px 8px;
}
.ms-note {
  margin-top: 6px;
  font-size: var(--size-min); line-height: var(--lh-desc);
  color: var(--muted);
}

/* ------------------------------------------------ loyalty, on the menu's code
   Quieter than the menu itself — it is an offer, not the thing the diner came
   for. Every colour resolves through the theme's own tokens so a shop that
   restyled its surfaces keeps its palette here too. */
.loyalty {
  width: 100%; max-width: var(--maxw); margin-inline: auto;
  padding: 14px var(--pad) 0;
}
html[data-layout="grid"] .loyalty { max-width: 1040px; }
.loyalty-h {
  font-size: var(--size-min); font-weight: 600;
  color: var(--muted); letter-spacing: var(--tracking); margin-bottom: 6px;
}
.loyalty-lead {
  font-size: var(--size-desc); line-height: var(--lh-desc); color: var(--text);
}
.loyalty-rewards { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
.loyalty-reward {
  font-size: var(--size-min); border-radius: 999px; padding: 3px 10px;
  background: var(--chip-bg, var(--tint)); color: var(--chip-text, var(--text));
}
.loyalty-join {
  display: inline-block; margin-top: 4px; padding: 10px 18px;
  min-height: 44px; line-height: 24px;          /* a real tap target */
  border-radius: var(--radius); text-decoration: none; font-weight: 700;
  background: var(--btn-bg, var(--brand)); color: var(--btn-text, #fff);
}
.loyalty-terms {
  margin-top: 6px; font-size: var(--size-min); color: var(--muted);
  line-height: var(--lh-desc);
}


/* THE PHOTO OPENS UNDER THE NAME ------------------------------------------
   The founder's call: tapping a dish photo used to throw a full-screen
   lightbox over the menu, which loses your place in a long list. It now
   EXPANDS IN PLACE, directly under the item's name, and collapses on a second
   tap.

   Animated with grid-template-rows 0fr -> 1fr rather than max-height: the
   panel's real height is whatever the photo needs, and a max-height guess is
   either a clipped picture or a lazy tail on the close. The inner wrapper's
   overflow:hidden is what makes the row animate at all. */
.item-photo {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .34s cubic-bezier(.22, .8, .3, 1),
              opacity .2s ease;
  opacity: 0;
}
.item-photo > div { overflow: hidden; min-height: 0; }
.item.is-open > .item-body .item-photo { grid-template-rows: 1fr; opacity: 1; }

.item-photo img {
  width: 100%; height: auto; display: block;
  margin-top: var(--s3);
  /* The EXPANDED photo wears the same shape and the same ring as its
     thumbnail — they are one object seen at two sizes, and the founder's
     screenshot showed them agreeing only by accident. A circle is capped at
     28px here: a full-width photograph clipped to an ellipse is not what
     "circle" means to somebody who picked it for a thumbnail. */
  border-radius: min(var(--shot-radius, var(--r-sm)), 28px);
  background: var(--shot-bg, var(--tint));
  border: 1px solid var(--shot-line, var(--line));
}
/* A cut-out has no background to frame — it floats, exactly as the thumbnail
   does, so the expanded view keeps the same treatment rather than putting the
   silhouette back in a box. */
.item-photo.is-cut img {
  /* Same rule as the thumbnail: the owner's backdrop wins, nothing else. */
  background: var(--shot-bg, transparent); border: 0;
  filter: drop-shadow(0 6px 14px color-mix(in srgb, var(--text) 26%, transparent));
}

/* The thumbnail says it can be opened, and says which state it is in. */
.shot[aria-expanded="true"] { outline: 2px solid var(--brand); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .item-photo { transition: none; }
}


/* =========================================================== LAYOUT: magazine
   Editorial: each dish is a wide spread — photograph on one side, words on
   the other — and the sides ALTERNATE, which is what makes a scroll read as a
   magazine rather than a list. On a phone it stacks; alternation is a
   wide-screen luxury, not a phone tax. */
.layout-magazine { --ratio: 4 / 3; }
.layout-magazine .items { gap: 22px; }
.layout-magazine .item {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-magazine .shot { width: 100%; border-radius: 0; }
.layout-magazine .item-body { padding: 16px 18px 18px; }
.layout-magazine .item-name { font-size: calc(var(--size-item) * 1.15); }
@media (min-width: 640px) {
  .layout-magazine .item { display: flex; align-items: stretch; }
  .layout-magazine .shot { width: 46%; flex: none; aspect-ratio: auto; min-height: 220px; }
  .layout-magazine .item-body {
    flex: 1; display: flex; flex-direction: column; justify-content: center;
    padding: 22px 24px;
  }
  /* THE ALTERNATION. nth-child, not a class from JS — the markup is the same
     markup every layout shares, and the rhythm is the stylesheet's job. */
  .layout-magazine .item:nth-child(even) { flex-direction: row-reverse; }
  .layout-magazine .item.no-media { display: block; }
}

/* =========================================================== LAYOUT: sheet
   A printed price sheet: dense, ruled, two columns when the paper is wide
   enough. Small square thumbs keep the food present without letting pictures
   take the page over — the PRICES are the content here. */
.layout-sheet { --ratio: 1 / 1; }
.layout-sheet .items { gap: 0; }
.layout-sheet .item {
  border-bottom: 1px solid var(--line);
  padding: 10px 2px;
  break-inside: avoid;
}
.layout-sheet .item:last-child { border-bottom: 0; }
.layout-sheet .shot { width: 40px; margin-top: 2px; }
.layout-sheet .item-name { font-size: calc(var(--size-item) * 0.95); }
@media (min-width: 720px) {
  .layout-sheet .items { display: block; column-count: 2; column-gap: 34px; }
  .layout-sheet .item { display: inline-block; width: 100%; }
}


/* =========================================================== LAYOUT: hero
   The photograph IS the card. A full-bleed shot bleeding down into its own
   caption panel — big type, one dish at a time on a phone, two-up when the
   paper is wide. No absolute overlay: the body FADES UP over the photo's
   bottom edge with a gradient into the surface, so the expandable content,
   the facts and the tags all keep their normal flow. */
.layout-hero { --ratio: 16 / 10; }
.layout-hero .items { gap: 20px; }
.layout-hero .item {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-hero .shot { width: 100%; border-radius: 0; }
.layout-hero .item-body { padding: 16px 18px 18px; }
.layout-hero .item:not(.no-media) .item-body {
  position: relative;
  margin-top: -46px;
  padding-top: 54px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--surface) 0%, transparent) 0,
    var(--surface) 46px);
}
.layout-hero .item-name { font-size: calc(var(--size-item) * 1.3); }
@media (min-width: 720px) {
  .layout-hero .items {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 20px;
    align-items: start;
  }
}

/* =========================================================== LAYOUT: mosaic
   A rhythm of tiles: every third dish takes the full width, the two after it
   share a row. The repetition is what reads as a designed page rather than a
   plain grid — and it is pure CSS on the shared markup, nth-child does all
   the arithmetic. */
.layout-mosaic { --ratio: 1 / 1; }
.layout-mosaic .items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  align-items: start;
}
.layout-mosaic .item {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-mosaic .shot { width: 100%; border-radius: 0; }
.layout-mosaic .item-body { padding: 12px 14px 14px; }
.layout-mosaic .item-name { font-size: calc(var(--size-item) * 0.95); }
.layout-mosaic .item:nth-child(3n + 1) { grid-column: 1 / -1; }
.layout-mosaic .item:nth-child(3n + 1) .shot { aspect-ratio: 16 / 9; }
.layout-mosaic .item:nth-child(3n + 1) .item-name {
  font-size: calc(var(--size-item) * 1.15);
}

/* ================================================= THE CENTERED FAMILY
   bistro, tasting, royale, folio, orbit and polaroid all put the dish on the
   page's centre line: the head stacks into a column, the price sits directly
   under the name instead of at the far edge, and everything a diner reads is
   centred. Shared here rather than copied six times, so the six can never
   drift apart on the one move they have in common.

   The `html[dir]` prefix is specificity, not decoration: the base sheet pins
   text-align to the page direction with html[dir="rtl"]/[dir="ltr"] rules, and
   a bare .layout-* selector loses that fight. Adding the attribute ties the
   score and lets source order decide — which is exactly these later rules. */
.layout-bistro .item-head, .layout-tasting .item-head, .layout-royale .item-head,
.layout-folio .item-head, .layout-orbit .item-head, .layout-polaroid .item-head {
  flex-direction: column; align-items: center; gap: 5px;
}
.layout-bistro .item-lead, .layout-tasting .item-lead, .layout-royale .item-lead,
.layout-folio .item-lead, .layout-orbit .item-lead, .layout-polaroid .item-lead {
  text-align: center;
}
.layout-bistro .item-price, .layout-tasting .item-price, .layout-royale .item-price,
.layout-folio .item-price, .layout-orbit .item-price, .layout-polaroid .item-price,
.layout-bistro .price-wrap, .layout-tasting .price-wrap, .layout-royale .price-wrap,
.layout-folio .price-wrap, .layout-orbit .price-wrap, .layout-polaroid .price-wrap {
  margin-inline-start: 0;
}
.layout-bistro .price-wrap, .layout-tasting .price-wrap, .layout-royale .price-wrap,
.layout-folio .price-wrap, .layout-orbit .price-wrap, .layout-polaroid .price-wrap {
  align-items: center;
}
.layout-bistro .price-line, .layout-tasting .price-line, .layout-royale .price-line,
.layout-folio .price-line, .layout-orbit .price-line, .layout-polaroid .price-line {
  justify-content: center;
}
html[dir] .layout-bistro .item-name-alt, html[dir] .layout-bistro .item-desc,
html[dir] .layout-tasting .item-name-alt, html[dir] .layout-tasting .item-desc,
html[dir] .layout-royale .item-name-alt, html[dir] .layout-royale .item-desc,
html[dir] .layout-folio .item-name-alt, html[dir] .layout-folio .item-desc,
html[dir] .layout-orbit .item-name-alt, html[dir] .layout-orbit .item-desc,
html[dir] .layout-polaroid .item-name-alt, html[dir] .layout-polaroid .item-desc {
  text-align: center;
}
.layout-bistro .tags, .layout-tasting .tags, .layout-royale .tags,
.layout-folio .tags, .layout-orbit .tags, .layout-polaroid .tags,
.layout-bistro .item-facts, .layout-tasting .item-facts, .layout-royale .item-facts,
.layout-folio .item-facts, .layout-orbit .item-facts, .layout-polaroid .item-facts {
  justify-content: center;
}

/* =========================================================== LAYOUT: bistro
   An elegant centred column: a round thumb over the name, the price directly
   beneath it, air instead of rules. The column caps at 620px so a wide screen
   reads it as a card stood on a table, not a banner. */
.layout-bistro { --ratio: 1 / 1; }
.layout-bistro .items { gap: 30px; max-width: 620px; margin-inline: auto; }
.layout-bistro .item { display: flex; flex-direction: column; align-items: center; }
.layout-bistro .item-body { width: 100%; }
.layout-bistro .shot { width: 72px; border-radius: 50%; margin-bottom: 10px; }

/* =========================================================== LAYOUT: banquet
   One grand row per dish: the name at half again its size with the price on
   its own line, a small thumb at the start, and a hairline between courses.
   The generosity is vertical — the structure is still a printed row. */
.layout-banquet { --ratio: 1 / 1; }
.layout-banquet .items { gap: 0; }
.layout-banquet .item {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 26px 2px;
  border-bottom: 1px solid var(--line);
}
.layout-banquet .item:last-child { border-bottom: 0; }
.layout-banquet .shot { width: 56px; margin-top: 5px; }
.layout-banquet .item-body { flex: 1 1 auto; min-width: 0; }
.layout-banquet .item-name { font-size: calc(var(--size-item) * 1.5); }
@media (min-width: 720px) {
  .layout-banquet .item { padding: 32px 2px; }
}

/* =========================================================== LAYOUT: tasting
   A numbered tasting menu: a small muted course number above each centred
   name, the price under it, big quiet gaps and no pictures at rest. The
   number is a CSS counter on the shared markup — the stylesheet does the
   counting, the same way mosaic does its rhythm. */
.layout-tasting .items {
  counter-reset: course;
  gap: 34px; max-width: 560px; margin-inline: auto;
}
.layout-tasting .item {
  counter-increment: course;
  display: flex; flex-direction: column; align-items: center;
}
.layout-tasting .item::before {
  content: counter(course, decimal-leading-zero);
  font-size: var(--size-min); font-weight: 600;
  letter-spacing: .14em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  margin-bottom: 6px;
}
.layout-tasting .item-body { width: 100%; }
.layout-tasting .shot { display: none; }

/* =========================================================== LAYOUT: deli
   The deli board: two text-only columns of small type — the most menu in the
   least space. Three columns once the paper is wide (this one runs in the
   wide shell). */
.layout-deli .items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 22px; row-gap: 0;
}
.layout-deli .item { padding: 8px 0; border-bottom: 1px solid var(--line); }
.layout-deli .shot { display: none; }
.layout-deli .item-name { font-size: max(var(--size-min), calc(var(--size-item) * .85)); }
.layout-deli .item-price { font-size: max(var(--size-min), calc(var(--size-price) * .85)); }
.layout-deli .item-desc { font-size: var(--size-min); }
@media (min-width: 720px) {
  .layout-deli .items { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* =========================================================== LAYOUT: polaroid
   Instant photographs pinned to the page: each dish in a white padded frame,
   tilted a degree and a half — odd tiles one way, even tiles the other — with
   its caption centred inside the frame. The frame is white BY SPEC, on every
   theme, so the ink inside is fixed too: theme text on white paper is
   unreadable the moment a dark palette is chosen.

   The tilt rides on the independent `rotate` property, NOT transform: the
   entrance animation fills `transform: none` forwards, which would silently
   flatten a transform rotation on every animated card. */
.layout-polaroid { --ratio: 1 / 1; }
.layout-polaroid .items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
  align-items: start;
}
.layout-polaroid .item {
  background: #fff;
  padding: 12px;
  box-shadow: 0 3px 10px rgba(0, 0, 0, .18);
  color: #262626;
}
.layout-polaroid .item:nth-child(odd) { rotate: 1.5deg; }
.layout-polaroid .item:nth-child(even) { rotate: -1.5deg; }
.layout-polaroid .shot { width: 100%; border-radius: 0; margin-bottom: 10px; }
.layout-polaroid .item-price { color: #262626; }
html[dir] .layout-polaroid .item-name-alt { color: #8a8a8a; }
html[dir] .layout-polaroid .item-desc { color: #6f6f6f; }
/* A dish with no photograph still gets a frame — it holds a brand-tint square
   where the picture would be, so the wall stays a wall. */
.layout-polaroid .item.no-media::before {
  content: "";
  display: block;
  aspect-ratio: 1 / 1;
  background: var(--tint-strong);
  margin-bottom: 10px;
}
@media (min-width: 720px) {
  .layout-polaroid .items { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .layout-polaroid .items { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* =========================================================== LAYOUT: ticket
   Every dish a stub: a dashed border for the perforation, a bold name, and
   the price in a solid brand tab pinned to the stub's top start corner — the
   tab reads the button tokens so an owner who restyled their buttons gets the
   same ink here. Two stubs to a row once the screen allows. */
.layout-ticket { --ratio: 1 / 1; }
.layout-ticket .items { gap: 14px; }
.layout-ticket .item {
  display: flex; align-items: flex-start; gap: 12px;
  border: 2px dashed var(--line);
  border-radius: var(--r-sm);
  padding: 36px 14px 14px;
}
.layout-ticket .shot { width: 54px; }
.layout-ticket .item-body { flex: 1 1 auto; min-width: 0; }
.layout-ticket .item-name { font-weight: 700; }
/* The child combinator matters: inside a discounted .price-wrap the inner
   price must NOT be positioned a second time. */
/* THE END CORNER, PHYSICALLY. The tab sits opposite the name's reading
   start — balanced corners, one look on every surface (the apps pin their
   tab to .topTrailing/TopEnd for the same reason). Logical insets would
   resolve against the PRICE's own ltr direction (digits are pinned ltr) and
   land the tab on the reading side, which is exactly the drift this avoids. */
.layout-ticket .item-head > .item-price,
.layout-ticket .item-head > .price-wrap {
  position: absolute;
  top: -2px; right: -2px;
  margin: 0;
  padding: 5px 12px;
  background: var(--btn-bg, var(--brand));
  color: var(--btn-text, #fff);
  border-radius: 0;
  border-top-right-radius: var(--r-sm);
  border-bottom-left-radius: var(--r-sm);
}
html[dir="rtl"] .layout-ticket .item-head > .item-price,
html[dir="rtl"] .layout-ticket .item-head > .price-wrap {
  right: auto; left: -2px;
  border-top-right-radius: 0;
  border-bottom-left-radius: 0;
  border-top-left-radius: var(--r-sm);
  border-bottom-right-radius: var(--r-sm);
}
.layout-ticket .item-head .item-price { color: var(--btn-text, #fff); }
.layout-ticket .item-head .item-price-old { color: var(--btn-text, #fff); opacity: .75; }
@media (min-width: 720px) {
  .layout-ticket .items {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
    align-items: start;
  }
}

/* =========================================================== LAYOUT: ledger
   The account book: zebra rows — odd rows tinted with the line colour at a
   third strength — a small thumb, and the price held hard at the end in the
   tabular numerals every price on this page already uses. */
.layout-ledger { --ratio: 1 / 1; }
.layout-ledger .items { gap: 0; }
.layout-ledger .item {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 10px;
}
.layout-ledger .item:nth-child(odd) {
  background: color-mix(in srgb, var(--line) 35%, transparent);
}
.layout-ledger .shot { width: 40px; margin-top: 2px; }
.layout-ledger .item-body { flex: 1 1 auto; min-width: 0; }

/* =========================================================== LAYOUT: gallery
   A wall of square photographs, three to a row — four in the wide shell —
   with the name and price sitting ON the picture over a dark scrim. The scrim
   and its white ink are the one hard-coded pair the spec allows: they must
   read over a photograph, which no theme token can promise.

   The caption strip lies over the bottom of the shot BUTTON, so it passes
   pointer events through — the whole tile stays the tap that opens the
   photo — except the facts row, which can carry its own tap targets. */
.layout-gallery { --ratio: 1 / 1; }
.layout-gallery .items {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
.layout-gallery .item { border-radius: var(--r-sm); overflow: hidden; }
.layout-gallery .shot { width: 100%; border-radius: 0; }
.layout-gallery .item-head { flex-direction: column; align-items: flex-start; gap: 2px; }
.layout-gallery .item-name { font-size: max(var(--size-min), calc(var(--size-item) * .82)); }
.layout-gallery .item-price {
  margin-inline-start: 0;
  font-size: max(var(--size-min), calc(var(--size-price) * .85));
}
.layout-gallery .price-wrap { margin-inline-start: 0; align-items: flex-start; }
.layout-gallery .price-line { justify-content: flex-start; }
.layout-gallery .item-desc, .layout-gallery .tags { display: none; }
.layout-gallery .item:not(.no-media) .item-body {
  position: absolute;
  inset-inline: 0; inset-block-end: 0;
  padding: 20px 9px 8px;
  background: linear-gradient(0deg, rgba(0, 0, 0, .66), rgba(0, 0, 0, .38) 60%, transparent);
  color: #fff;
  pointer-events: none;
}
.layout-gallery .item:not(.no-media) .item-facts { pointer-events: auto; }
.layout-gallery .item:not(.no-media) .item-price { color: #fff; }
.layout-gallery .item:not(.no-media) .item-name-alt { color: #fff; opacity: .8; }
.layout-gallery .item:not(.no-media) .includes li { color: #fff; }
/* No photograph: a brand-tint tile with the name centred in it. */
.layout-gallery .item.no-media {
  aspect-ratio: 1 / 1;
  background: var(--tint-strong);
  display: flex; align-items: center; justify-content: center;
}
.layout-gallery .item.no-media .item-body { padding: 8px; }
.layout-gallery .item.no-media .item-head { align-items: center; }
.layout-gallery .item.no-media .item-lead { text-align: center; }
@media (min-width: 720px) {
  .layout-gallery .items { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* =========================================================== LAYOUT: panorama
   A very wide strip per dish — a 21:9 crop with the name and price on a scrim
   band across the image's foot, and the words underneath the picture where
   they can breathe. The band overlaps the photo by exactly its own height, so
   the caption is ON the food without an absolute overlay swallowing the rest
   of the body. A dish with no photo renders as the photo layout's text card. */
.layout-panorama { --ratio: 21 / 9; }
.layout-panorama .items { gap: 16px; }
.layout-panorama .item {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-panorama .shot { width: 100%; border-radius: 0; }
.layout-panorama .item-body { padding: 0 16px 16px; }
.layout-panorama .item:not(.no-media) .item-body { margin-top: -48px; }
.layout-panorama .item:not(.no-media) .item-head {
  min-height: 48px;
  align-items: center;
  margin: 0 -16px 12px;
  padding: 10px 16px;
  background: linear-gradient(0deg, rgba(0, 0, 0, .62), rgba(0, 0, 0, .3));
  color: #fff;
}
.layout-panorama .item:not(.no-media) .item-head .item-price { color: #fff; }
.layout-panorama .item:not(.no-media) .item-head .item-name-alt { color: #fff; opacity: .85; }
.layout-panorama .item.no-media { border-inline-start: 3px solid var(--accent); }
.layout-panorama .item.no-media .item-body { padding: 16px; }

/* =========================================================== LAYOUT: duet
   Photo beside the words, ALWAYS — even on a phone — two balanced halves of
   one card. No alternation: that is magazine's move; duet's point is the
   steady rhythm of picture-then-text on every row. */
.layout-duet { --ratio: 1 / 1; }
.layout-duet .items { gap: 14px; }
.layout-duet .item {
  display: flex; align-items: stretch;
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-duet .shot {
  width: 42%; flex: none;
  aspect-ratio: auto; min-height: 150px;
  border-radius: 0;
}
.layout-duet .item-body {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; justify-content: center;
  padding: 14px 16px;
}
.layout-duet .item.no-media { display: block; border-inline-start: 3px solid var(--accent); }

/* =========================================================== LAYOUT: royale
   Centred, tracked and upper-cased where the alphabet has cases — Arabic
   simply keeps its dignity — with a double rule between dishes and no
   pictures at rest. The letterhead of a grand hotel. */
.layout-royale .items { gap: 0; max-width: 560px; margin-inline: auto; }
.layout-royale .item {
  display: flex; flex-direction: column; align-items: center;
  padding: 24px 2px;
  border-bottom: 3px double var(--line);
}
.layout-royale .item:last-child { border-bottom: 0; }
.layout-royale .item-body { width: 100%; }
.layout-royale .shot { display: none; }
.layout-royale .item-name { text-transform: uppercase; letter-spacing: .08em; }

/* =========================================================== LAYOUT: street
   Street food: names shouted extra-bold at a third again their size, the
   price in a filled brand pill at the row's end, tags allowed to be loud.
   No pictures at rest — the type IS the sign. */
.layout-street .items { gap: 4px; }
.layout-street .item { padding: 10px 2px; }
.layout-street .shot { display: none; }
.layout-street .item-name { font-size: calc(var(--size-item) * 1.35); font-weight: 800; }
.layout-street .item-price {
  background: var(--btn-bg, var(--brand));
  color: var(--btn-text, #fff);
  padding: 4px 12px;
  border-radius: var(--r-pill);
  font-size: max(var(--size-min), calc(var(--size-price) * .9));
}
.layout-street .tag { font-weight: 700; border-color: var(--accent); }

/* =========================================================== LAYOUT: diner
   The American diner menu: leader dots running from the name to the price —
   m.js emits the same leader span it gives the list layout — a 48px thumb,
   and the description in italics right under the name, because at a diner
   the description is half the fun. */
.layout-diner { --ratio: 1 / 1; }
.layout-diner .items { gap: 0; }
.layout-diner .item {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 2px;
  border-bottom: 1px solid var(--line);
}
.layout-diner .item:last-child { border-bottom: 0; }
.layout-diner .shot { width: 48px; margin-top: 2px; }
.layout-diner .item-body { flex: 1 1 auto; min-width: 0; }
.layout-diner .item-head { align-items: baseline; gap: 6px; }
.layout-diner .item-head:has(.price-wrap) { align-items: flex-start; }
.layout-diner .price-wrap { padding-top: 1px; }
.layout-diner .leader {
  flex: 1 1 auto;
  min-width: 14px;
  height: 0;
  border-bottom: 1px dotted var(--line);
  transform: translateY(-4px);
}
.layout-diner .item-desc { font-style: italic; }

/* =========================================================== LAYOUT: espresso
   Rows as short as the drink: name and price on one hairline-ruled line,
   everything else put away at rest. For the counter queue, not the corner
   table. */
.layout-espresso .items { gap: 0; }
.layout-espresso .item {
  min-height: var(--tap);
  padding: 10px 2px;
  border-bottom: 1px solid var(--line);
}
.layout-espresso .item:last-child { border-bottom: 0; }
.layout-espresso .shot, .layout-espresso .item-desc, .layout-espresso .tags { display: none; }

/* =========================================================== LAYOUT: folio
   A cookbook page per dish: the full 3:2 photograph, then name, price and
   description all centred beneath it, with the big gaps of a book that is
   not in a hurry. A dish with no photo is simply a centred page of text. */
.layout-folio { --ratio: 3 / 2; }
.layout-folio .items { gap: 36px; }
.layout-folio .item { display: flex; flex-direction: column; align-items: center; }
.layout-folio .shot { width: 100%; }
.layout-folio .item-body { width: 100%; padding: 14px 8px 0; }

/* =========================================================== LAYOUT: orbit
   Round photographs three across, each dish's name and price centred under
   its circle. A dish with no photo gets a brand-tint circle holding its
   first letter — m.js stamps `data-initial` on photo-less items exactly so
   this layout can draw it without new markup. */
.layout-orbit { --ratio: 1 / 1; }
.layout-orbit .items {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px 12px;
  align-items: start;
}
.layout-orbit .item { display: flex; flex-direction: column; align-items: center; }
.layout-orbit .item-body { width: 100%; }
.layout-orbit .shot { width: 100%; border-radius: 50%; margin-bottom: 8px; }
.layout-orbit .item-name { font-size: max(var(--size-min), calc(var(--size-item) * .9)); }
.layout-orbit .item.no-media::before {
  content: attr(data-initial);
  display: flex; align-items: center; justify-content: center;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  background: var(--tint-strong);
  color: var(--brand);
  font-family: var(--heading);
  font-weight: 700;
  font-size: calc(var(--size-section) * 1.2);
  margin-bottom: 8px;
}
@media (min-width: 720px) {
  .layout-orbit .items { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .layout-orbit .items { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* =========================================================== LAYOUT: band
   The photograph is the row's own background: the shot stretches to fill a
   96px band — 128px in the wide shell — and the words sit above it on a
   start-side scrim, in the white ink the spec fixes for text over
   photographs. The body passes pointer events through, so the whole band is
   still the button that opens the photo; a dish with no photo is a
   brand-tint band in the theme's own ink. */
.layout-band .items { gap: 10px; }
.layout-band .item {
  min-height: 96px;
  display: flex; align-items: stretch;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--tint-strong);
}
.layout-band .shot {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  aspect-ratio: auto;
  border-radius: 0;
}
.layout-band .item-body {
  position: relative;
  width: 100%;
  display: flex; flex-direction: column; justify-content: center;
  padding: 12px 16px;
  pointer-events: none;
}
.layout-band .item-facts { pointer-events: auto; }
.layout-band .item:not(.no-media) .item-body { color: #fff; }
/* The scrim hugs the START side — where the words are — and follows the page
   direction, while the photograph itself never mirrors. */
html[dir="ltr"] .layout-band .item:not(.no-media) .item-body {
  background: linear-gradient(to right, rgba(0, 0, 0, .66), rgba(0, 0, 0, .36) 48%, transparent 82%);
}
html[dir="rtl"] .layout-band .item:not(.no-media) .item-body {
  background: linear-gradient(to left, rgba(0, 0, 0, .66), rgba(0, 0, 0, .36) 48%, transparent 82%);
}
.layout-band .item:not(.no-media) .item-price { color: #fff; }
.layout-band .item:not(.no-media) .item-name-alt { color: #fff; opacity: .8; }
.layout-band .item:not(.no-media) .includes li { color: #fff; }
.layout-band .item-desc, .layout-band .tags { display: none; }
@media (min-width: 720px) {
  .layout-band .item { min-height: 128px; }
}

/* =========================================================== LAYOUT: terrace
   Two-up cards under a 4px brand accent bar — the awning over each table —
   with a square photo, roomier padding than the grid gives, and a soft
   shadow. */
.layout-terrace { --ratio: 1 / 1; }
.layout-terrace .items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  align-items: start;
}
.layout-terrace .item {
  display: flex; flex-direction: column;
  background: var(--surface);
  border-top: 4px solid var(--accent);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-terrace .shot { width: 100%; border-radius: 0; }
.layout-terrace .item-body { padding: 16px; display: flex; flex-direction: column; }
.layout-terrace .item-head { flex-direction: column; align-items: stretch; gap: 6px; }
.layout-terrace .item-name { font-size: max(var(--size-min), calc(var(--size-item) * .95)); }
.layout-terrace .item-price { align-self: flex-start; margin-inline-start: 0; }
.layout-terrace .price-wrap { align-self: flex-start; align-items: flex-start; }
.layout-terrace .price-line { justify-content: flex-start; }
@media (min-width: 720px) {
  .layout-terrace .items { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .layout-terrace .items { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* =========================================================== LAYOUT: souk
   The market stall: dense rows with a thick accent border on the start side
   and a faint tint over each one, a 44px thumb, the price at the end. Busy
   on purpose — a souk is not minimal. */
.layout-souk { --ratio: 1 / 1; }
.layout-souk .items { gap: 6px; }
.layout-souk .item {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px;
  background: var(--tint);
  border-inline-start: 4px solid var(--accent);
  border-radius: var(--r-sm);
}
.layout-souk .shot { width: 44px; margin-top: 2px; }
.layout-souk .item-body { flex: 1 1 auto; min-width: 0; }

/* =========================================================== LAYOUT: platter
   The first dish of every section is the hero — a big photo card at a fifth
   over size — and the rest queue up as printed-list rows beneath it. Pure
   nth arithmetic on the shared markup: the section's own order decides who
   gets the platter. */
.layout-platter { --ratio: 16 / 10; }
.layout-platter .items { gap: 0; }
.layout-platter .item:first-child {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  margin-bottom: 8px;
}
.layout-platter .item:first-child .shot { width: 100%; border-radius: 0; }
.layout-platter .item:first-child .item-body { padding: 16px 18px 18px; }
.layout-platter .item:first-child:not(.no-media) .item-body {
  position: relative;
  margin-top: -46px;
  padding-top: 54px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--surface) 0%, transparent) 0,
    var(--surface) 46px);
}
.layout-platter .item:first-child .item-name { font-size: calc(var(--size-item) * 1.2); }
.layout-platter .item:first-child.no-media { border-inline-start: 3px solid var(--accent); }
.layout-platter .item:not(:first-child) {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 15px 2px;
  border-bottom: 1px solid var(--line);
}
.layout-platter .item:not(:first-child):last-child { border-bottom: 0; }
.layout-platter .item:not(:first-child) .shot { width: 54px; margin-top: 2px; aspect-ratio: 1 / 1; }
.layout-platter .item:not(:first-child) .item-body { flex: 1 1 auto; min-width: 0; }

/* =========================================================== LAYOUT: minimal
   Name and price and nothing else at rest, with double the printed list's
   row padding — white space as the design. Everything a dish knows is still
   in the markup for the day the diner asks. */
.layout-minimal .items { gap: 0; }
.layout-minimal .item {
  padding: 30px 2px;
  border-bottom: 1px solid var(--line);
}
.layout-minimal .item:last-child { border-bottom: 0; }
.layout-minimal .shot, .layout-minimal .item-desc, .layout-minimal .tags { display: none; }

/* =========================================================== LAYOUT: checker
   A chessboard of image and words: two-up tiles where the odd tile draws its
   photo above the text and the even tile draws it below — nth-child flips
   the order, the markup never changes. Tiles stretch to their row so the
   board keeps its straight seams. */
.layout-checker { --ratio: 1 / 1; }
.layout-checker .items {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
.layout-checker .item {
  display: flex; flex-direction: column;
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.layout-checker .shot { width: 100%; border-radius: 0; }
.layout-checker .item:nth-child(even) .shot { order: 2; margin-top: auto; }
.layout-checker .item-body { flex: 1 1 auto; display: flex; flex-direction: column; padding: 12px; }
.layout-checker .item-head { flex-direction: column; align-items: stretch; gap: 6px; }
.layout-checker .item-name { font-size: max(var(--size-min), calc(var(--size-item) * .9)); }
.layout-checker .item-price {
  align-self: flex-start; margin-inline-start: 0;
  font-size: max(var(--size-min), calc(var(--size-price) * .94));
}
.layout-checker .price-wrap { align-self: flex-start; align-items: flex-start; }
.layout-checker .price-line { justify-content: flex-start; }
.layout-checker .item.no-media { min-height: 118px; }
.layout-checker .item.no-media .item-body { justify-content: center; }
@media (min-width: 720px) {
  .layout-checker .items { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  .layout-checker .items { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}


/* 3-UP TILES ARE QUIET TILES. Gallery and orbit squeeze three dishes into a
   row; the kcal/allergen facts row that fits under a list row turns these
   tiles into confetti. The APPS' gallery/orbit already keep the facts behind
   the tap — the web tile does the same, and the tap that opens the photo is
   the same tap that reveals them. Name and price never hide (tested). */
.layout-gallery .item-facts, .layout-gallery .includes,
.layout-orbit .item-facts, .layout-orbit .includes {
  display: none;
}
.layout-gallery .item.is-open .item-facts, .layout-gallery .item.is-open .includes,
.layout-orbit .item.is-open .item-facts, .layout-orbit .item.is-open .includes {
  display: block;
}


/* The optional word on a dish OPTION — «موسمي», «حار». The main tag's own
   pill, one size smaller, sitting right BESIDE the option's name (the price
   holds the row's end on its own). The svg cap matters: an uncapped glyph
   renders at its natural size and swallows the row. */
.variant-tag {
  display: inline-flex; align-items: center; gap: 4px;
  flex: none;
  padding: 2px 8px;
  font-size: 11px;
  border-radius: 999px;
  background: var(--badge-bg, var(--tint));
  color: var(--badge-text, var(--brand));
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
}
.variant-tag svg { width: 12px; height: 12px; flex: none; }
.choice-tag {
  margin-inline-start: 6px;
  padding-inline-start: 6px;
  border-inline-start: 1px solid var(--line);
  font-size: 10.5px;
  color: var(--muted);
}


/* ---------------------------------------------------------------- share */
/* THE SHARE BUTTON, BOTTOM-LEFT.

   `left`, not `inset-inline-start`: the founder asked for the left corner and
   meant the corner, so it does not swap sides when the page flips to English.
   The language switch is fixed top-end, so the two controls never meet.

   It rides above the page (z-index 60, the same layer as the language switch)
   and the footer below carries extra bottom padding to match, so the «عبر
   ألوكي» line is never sitting underneath it. */
.share {
  position: fixed;
  left: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom));
  z-index: 60;
  /* Shrink-wrapped to the button (a fixed box with auto width does that on its
     own), so the list below can hang off the button's own LEFT edge. It used
     to be a flex column with `align-items: flex-start` — which in RTL is the
     RIGHT edge, so the wide list pushed the button inwards and the corner the
     founder asked for held a menu instead of a button. */
}
.share-btn {
  display: inline-flex; align-items: center; gap: 8px;
  /* The same 44px floor every other control on this page stands on. */
  min-height: var(--tap); height: var(--tap);
  padding: 0 16px;
  border-radius: var(--r-pill);
  background: var(--brand);
  color: var(--surface);
  border: 1px solid transparent;
  box-shadow: var(--shadow);
  font-family: inherit;
  font-size: var(--size-min); font-weight: 600; letter-spacing: .02em;
  cursor: pointer;
}
.share-btn svg { width: 18px; height: 18px; flex: none; }
.share-btn:active { transform: scale(.96); }
@media (hover: hover) {
  .share-btn:hover { filter: brightness(1.06); }
}
/* The label is the only part that goes on a very narrow phone — the mark on
   its own is still a 44px target and still says what it does to a screen
   reader, because the button keeps its accessible name either way. */
@media (max-width: 340px) {
  .share-label { display: none; }
  .share-btn { padding: 0; width: var(--tap); justify-content: center; }
}

/* The DESKTOP fallback list. It only ever exists where navigator.share does
   not, so a phone never renders a single node of it. */
.share-menu {
  /* Anchored to the button's physical left edge and sitting just above it, in
     both languages — `left`, never `inset-inline-start`. */
  position: absolute;
  left: 0;
  bottom: calc(100% + 8px);
  display: flex; flex-direction: column;
  min-width: 190px;
  padding: 6px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
}
.share-menu a, .share-menu button {
  display: flex; align-items: center; gap: 10px;
  min-height: var(--tap);
  padding: 0 12px;
  border: 0; background: none;
  border-radius: var(--r-sm);
  color: var(--text);
  font-family: inherit; font-size: var(--size-min); font-weight: 500;
  text-align: start; text-decoration: none;
  cursor: pointer;
}
.share-menu svg { width: 18px; height: 18px; flex: none; color: var(--brand); }
.share-menu a:active, .share-menu button:active { background: var(--tint); }
@media (hover: hover) {
  .share-menu a:hover, .share-menu button:hover { background: var(--tint); }
}
/* «تم نسخ الرابط» — the copy button says so itself rather than firing a toast
   that would need its own layer and its own timing. */
.share-menu .is-done { color: var(--brand); font-weight: 600; }

/* Room for the button, so it never sits on top of the last thing on the page. */
.foot { padding-bottom: calc(76px + env(safe-area-inset-bottom)); }

/* -------------------------------------------------------------- loyalty */
/* THE JOIN BUTTON, BOTTOM-RIGHT — the share button's mirror image.

   `right`, not `inset-inline-end`, for the same reason `.share` uses `left`:
   the founder asked for the corners and meant the corners, so neither control
   swaps sides when the page flips to English and the two can never collide.
   It borrows `.share-btn` outright rather than restating it, so the pair stay
   one control in two places — a second set of paddings here is how they drift
   apart the first time either is touched. */
.loyal {
  position: fixed;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom));
  z-index: 60;
}
/* NO BREAKPOINT OF ITS OWN. The label span carries `.share-label` and the
   button carries `.share-btn`, so the narrow-phone rule below — label away,
   circle at 44px, at ≤340px — already governs both. A second breakpoint here
   is how the pair stopped matching the first time: this button dropped its
   word at 380px while «مشاركة» kept hers, on the very screen the founder
   asked for two buttons in the same style. */

.loyal-sheet { position: fixed; inset: 0; z-index: 70; }
.loyal-scrim {
  position: absolute; inset: 0;
  background: rgba(12, 10, 20, .46);
  backdrop-filter: blur(2px);
}
/* A SHEET ON A PHONE, a centred card on a desktop — the shape each medium
   already expects. `max-height` with its own scroll because a shop may write
   long terms, and terms that cannot be read are terms that were not shown.

   86%, NOT 86vh — the founder's blank-QR kiosks (2026-08-27). The loyalty
   app's kiosk WebView ignores the viewport meta (useWideViewPort is off),
   and in that mode EVERY vh-family unit — vh, svh, dvh — computes to 0px
   while vw stays correct; `min(86vh, 720px)` therefore computed to 0 and
   the card collapsed to its own padding, clipping a perfectly-loaded QR.
   The percentage resolves against `.loyal-sheet` itself — position:fixed,
   inset:0, so its height IS the viewport's in every engine, including that
   WebView (proven by CDP against the real app) — the same number a working
   vh would give, from a box that cannot lie. Do not reintroduce vh here. */
.loyal-sheet .loyalty {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 0; width: min(560px, 100%);
  max-height: min(86%, 720px); overflow-y: auto;
  border-radius: 20px 20px 0 0;
  padding: 22px 20px calc(22px + env(safe-area-inset-bottom));
  background: var(--surface);
  box-shadow: 0 -12px 40px rgba(0, 0, 0, .22);
}
@media (min-width: 640px) {
  .loyal-sheet .loyalty {
    bottom: auto; top: 50%; transform: translate(-50%, -50%);
    border-radius: 20px;
    padding-bottom: 22px;
  }
}
.loyal-close {
  position: absolute; inset-inline-end: 12px; top: 12px;
  width: 36px; height: 36px; display: grid; place-items: center;
  border-radius: 50%; border: 1px solid var(--hair);
  background: var(--surface); color: var(--ink); cursor: pointer;
}
.loyal-close svg { width: 18px; height: 18px; }

/* ---------------------------------------------------------------- kiosk */
/* KIOSK MODE (?kiosk=1). Only what the two QR sheets need beyond the
   `.loyal-sheet` anatomy they reuse: a centred card, a code big enough to
   scan from standing distance, and one quiet instruction under it. The
   buttons themselves add NOTHING here — they wear `.share-btn` inside the
   `.share`/`.loyal` corner wrappers, one style, now three users, exactly so
   there is no fourth set of paddings to drift. */
.kiosk-card { text-align: center; }
.kiosk-qr {
  display: block;
  width: min(420px, 78vw); height: auto;
  margin: 18px auto 10px;
  /* NO hardcoded white here (the theme-colours-only rule): the PNG itself
     carries its own white modules AND its own baked-in quiet zone, which is
     what a scanner actually needs — the frame around it may be the theme's. */
  background: var(--surface);
  padding: 10px;
  border-radius: 12px;
  border: 1px solid var(--line);
}
.kiosk-hint {
  margin: 0;
  color: var(--muted);
  font-size: var(--size-min);
}


/* THE ADD-ONS («الإضافات», 2026-09-20) — under the variants, in the facts row.
   The group's name is a quiet heading; its rule rides beside it in the muted
   ink; each option is a row with its supplement on the page's price token,
   or the word «ببلاش» where there is none. */
.item-addons {
  flex-basis: 100%;
  margin-top: 4px;
}
.addons-h {
  margin: 0 0 3px;
  font-size: var(--size-min); font-weight: 700; color: var(--text);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
}
.addons-rule { font-weight: 500; color: var(--muted); }
.addon-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 3px;
}
.addon-row {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--size-desc); line-height: var(--lh-desc);
  color: var(--text);
}
.addon-name { min-width: 0; overflow-wrap: anywhere; }
.addon-price {
  flex: none; margin-inline-start: auto;
  font-size: var(--size-min); font-weight: 600;
  color: var(--price-color, var(--brand));
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.addon-price .cur { font-size: var(--size-min); font-weight: 500; opacity: .75; margin-inline-start: 3px; }
.addon-free {
  flex: none; margin-inline-start: auto;
  font-size: var(--size-min); font-weight: 600; color: var(--muted);
}

/* THE SEARCH ENGINES' TEXT MENU (api/menupage.py, menu_seo.body_html). The same
   menu as plain words in the first response. Hidden while m.js runs the page
   (js-on.js marks <html>), shown again if the menu could not load (m.js adds
   `ssr-show` with the error screen), and always shown without scripts. */
.seo-menu { max-width: 720px; margin: 0 auto; padding: 24px 20px 48px; font: 400 16px/1.8 "Readex Pro", system-ui, sans-serif; color: var(--text); }
.seo-menu h1 { font-size: 28px; margin: 0 0 4px; }
.seo-menu h2 { font-size: 20px; margin: 28px 0 10px; }
.seo-menu h3 { font-size: 17px; margin: 20px 0 8px; }
.seo-menu h4 { font-size: 15px; margin: 0; }
.seo-menu ul { list-style: none; margin: 0; padding: 0; }
.seo-menu li { padding: 10px 0; border-bottom: 1px solid var(--line); }
.seo-menu p { margin: 2px 0; }
.seo-menu .seo-price { font-weight: 600; }
.seo-menu .seo-en, .seo-menu .seo-where, .seo-menu .seo-by { color: var(--muted); }
.seo-menu a { color: inherit; }
.js .seo-menu { display: none; }
.js.ssr-show .seo-menu { display: block; }
