/* A published menu.
 *
 * Its own stylesheet, not the app's: this page belongs to the venue, is read on
 * a phone at a table, and shares nothing with the builder but the fact that we
 * host it. Four custom properties come from the menu itself - the template sets
 * them and the client can edit every one - and everything below is written in
 * terms of those four, so a palette change is four numbers and no new CSS.
 *
 * The rest of the look arrives the same way: two font stacks and how headings
 * are set (`--font-heading`, `--font-body`, `--heading-*`), and the texture's
 * strength. The layout and the texture themselves are attributes on <body>,
 * read by the two sections of that name near the end. All of it is printed by
 * `menus/_look_style.html`; the fallbacks here are the original look, so a
 * page without them is the menu as it always was.
 *
 * Mobile first throughout, because that is not the smaller case here, it is the
 * only case that matters: nobody scans a code on a table with a laptop.
 */

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

/* An SVG fills black unless told otherwise, and this page is standalone - it
   does not load the app's stylesheet, so the `.icon` rule that makes every
   glyph follow its surroundings has to exist here too. Without it the clock,
   the marks and the network icons were drawn in black on every template: the
   colour set on the button reached only its ring, and on the dark preset a
   black glyph on a near-black page was simply not there. */
.icon {
  width: 1em; height: 1em;
  fill: currentColor;
  flex: none;
  vertical-align: -0.125em;
  /* Bootstrap's marks touch the edge of their viewBox and an <svg> clips at its
     viewport, so at a fractional em size a ring loses its bottom edge. */
  overflow: visible;
}

body {
  margin: 0;
  /* Room under everything for the currency pill, which is fixed to the corner:
     at 96px it sat on top of the contact icons at the foot of a short menu. */
  padding: 0 0 132px;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
  font-size: 17px;
  line-height: 1.45;
  /* A menu is read at arm's length in poor light. Nothing here goes below 15px,
     and the body copy is deliberately larger than the app's. */
  -webkit-text-size-adjust: 100%;
}

/* --- The venue ------------------------------------------------------------- */
.venue { padding: 28px 20px 18px; text-align: center; }
.venue-logo {
  display: block; max-width: 160px; max-height: 88px;
  margin: 0 auto 14px; object-fit: contain;
}
.venue h1 {
  margin: 0; font-size: calc(1.6rem * var(--heading-scale, 1));
  font-family: var(--font-heading, inherit); font-weight: var(--heading-weight, 700);
  letter-spacing: -0.01em; line-height: 1.2;
}
/* The name and the pencil after it, centred together. The pencil is in the
   muted colour so it reads as the venue's tool rather than part of its name,
   and its target is 32px whatever the glyph's size. */
.venue-name {
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.venue-name h1 { min-width: 0; }
.venue-edit {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 999px; flex: none;
  color: var(--muted);
}
.venue-edit:hover, .venue-edit:focus-visible {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.venue-edit .icon { width: 16px; height: 16px; }
.venue-note { margin: 6px 0 0; color: var(--muted); font-size: 0.95rem; }
/* The hours, as a chip under the name: a fact about the place rather than a
   line of the address. Tinted in the accent so it reads at a glance in a dim
   room, which is where this question gets asked. */
.venue-hours {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 12px 0 0; padding: 5px 13px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent); font-size: 0.86rem; font-weight: 600;
}
.venue-hours .icon { width: 15px; height: 15px; flex: none; }
/* "Since 1987" - under the name in small spaced capitals, quieter than the
   subtitle: it is a fact the venue is proud of, not a line to read. */
.venue-since {
  margin: 8px 0 0; color: var(--muted);
  font-size: 0.8rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
}

/* The cover: a 5:2 band across the top, edge to edge on a phone, and the logo
   lifted onto its lower edge on a tile of the page's own colour - so a logo
   drawn for a white page still sits on one, whatever the photograph is. The
   ratio is set here as well as by the file, because a file smaller than 1440
   is never enlarged on the server and this keeps every cover the same shape. */
.venue.has-cover { padding-top: 0; }
.venue-cover {
  display: block; width: calc(100% + 40px); max-width: none; height: auto;
  margin: 0 -20px 18px; aspect-ratio: 5 / 2; object-fit: cover;
  background: color-mix(in srgb, var(--ink) 6%, var(--surface));
}
.has-cover .venue-logo {
  position: relative; margin-top: -58px;
  padding: 8px; border-radius: 16px; background: var(--surface);
  box-shadow: 0 8px 22px rgb(0 0 0 / 0.18);
}

/* --- Tabs -------------------------------------------------------------------
   Radio inputs with their panels as siblings: one panel is shown by the checked
   input, with no script at all. The inputs themselves are off-screen rather than
   `display: none`, which would take them out of the keyboard's reach. */
.tab-input {
  position: absolute; width: 1px; height: 1px; opacity: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.tabs {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 0 16px 4px; justify-content: center;
  position: sticky; top: 0; z-index: 5;
  background: var(--surface);
  /* The tabs stay put while the list scrolls under them - a long menu is a lot
     of scrolling to get back to the top of otherwise. */
  padding-top: 10px; padding-bottom: 10px;
}
.tab {
  /* inline-flex so the glyph and the word sit on one line with a gap between
     them. Every tab carries one - a card, a percent or a calendar - see
     `Section.tab_icon`. */
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 16px; border-radius: 999px; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  color: var(--muted); font-size: 0.95rem; font-weight: 600;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.tab .icon { width: 1em; height: 1em; flex: none; opacity: 0.9; }
.tab:hover { border-color: var(--accent); color: var(--ink); }
.panel { display: none; width: 100%; }

/* The checked input styles its own label and its own panel. `:has` keeps this to
   two rules however many tabs a menu has. */
.tab-input:checked + .tab {
  background: var(--accent); border-color: var(--accent);
  color: var(--surface);
}
.tab-input:focus-visible + .tab { outline: 2px solid var(--accent); outline-offset: 2px; }
.tabs:has(.tab-input:nth-of-type(1):checked) .panel:nth-of-type(1),
.tabs:has(.tab-input:nth-of-type(2):checked) .panel:nth-of-type(2),
.tabs:has(.tab-input:nth-of-type(3):checked) .panel:nth-of-type(3),
.tabs:has(.tab-input:nth-of-type(4):checked) .panel:nth-of-type(4),
.tabs:has(.tab-input:nth-of-type(5):checked) .panel:nth-of-type(5),
.tabs:has(.tab-input:nth-of-type(6):checked) .panel:nth-of-type(6),
.tabs:has(.tab-input:nth-of-type(7):checked) .panel:nth-of-type(7),
.tabs:has(.tab-input:nth-of-type(8):checked) .panel:nth-of-type(8),
/* Raised from eight when offers and events became tabs of their own: a venue
   already using six for its menu is two away from the cap without adding a
   thing. There is no way to write this without one rule per position, so the
   number is simply generous - a tab bar with more than twelve on it is a
   different problem. */
.tabs:has(.tab-input:nth-of-type(9):checked) .panel:nth-of-type(9),
.tabs:has(.tab-input:nth-of-type(10):checked) .panel:nth-of-type(10),
.tabs:has(.tab-input:nth-of-type(11):checked) .panel:nth-of-type(11),
.tabs:has(.tab-input:nth-of-type(12):checked) .panel:nth-of-type(12) { display: block; }

/* --- Categories and items --------------------------------------------------- */
.menu { max-width: 620px; margin: 0 auto; padding: 0 16px; }
.panel > .category:first-of-type { margin-top: 4px; }
/* Each heading is a <details>, closed until a guest opens it - see the note in
   public.html.

   A block of its own, with a ground a shade off the page and real space between
   one and the next. It was a hairline rule between headings first, and that was
   not enough: a column of headings separated by a 1px line reads as one long
   list with some bold words in it, and a guest scanning for "Vino" had to read
   rather than look.

   Deliberately *not* the striping this was first asked for - every other block
   a shade darker. Two things go wrong with it here. An open heading's items
   inherit its stripe, so the moment somebody opens one the alternation stops
   meaning "new section" and starts meaning nothing; and the colours shuffle
   whenever the venue reorders a heading, so the same section is light one week
   and dark the next. Giving every block the same ground and putting *space*
   between them says the same thing without either. */
.category {
  margin: 0 0 10px;
  border: 1px solid color-mix(in srgb, var(--ink) 11%, transparent);
  border-radius: 14px;
  /* Mixed into the surface rather than into `transparent`: the same colour on a
     plain page, and an opaque one on a textured page - so the grain stops at
     the card's edge instead of adding itself to the card's own tint. See
     "Textures" below for why that sum is the thing to avoid. */
  background: color-mix(in srgb, var(--ink) 3.5%, var(--surface));
  overflow: hidden;
}

/* The open one is lifted out of the stack: a stronger ground, the accent on its
   border. With twelve headings closed and one open, that is the only thing on
   screen a guest is reading, and it should look like it. */
.category[open] {
  background: color-mix(in srgb, var(--ink) 6%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.category-head {
  display: flex; align-items: center; gap: 10px;
  padding: 15px 16px; cursor: pointer;
  /* The browser's own disclosure triangle, removed in both dialects. The caret
     below replaces it, on the other side - where the eye ends its scan of the
     heading rather than where it starts. */
  list-style: none;
}
.category-head::-webkit-details-marker { display: none; }
.category-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }

.category h2 {
  flex: 1; margin: 0; font-size: calc(1.05rem * var(--heading-scale, 1));
  font-family: var(--font-heading, inherit); font-weight: var(--heading-weight, 700);
  letter-spacing: var(--heading-tracking, 0.06em);
  text-transform: var(--heading-case, uppercase); color: var(--accent);
  line-height: 1.25;
}

/* How much is behind the heading, so a closed one is not a question. */
.category-count {
  flex: none; min-width: 22px; padding: 2px 7px; border-radius: 999px;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  color: var(--muted); font-size: 0.72rem; font-weight: 700;
  font-variant-numeric: tabular-nums; text-align: center;
}

.category-caret {
  flex: none; width: 15px; height: 15px; color: var(--muted);
  transition: transform 0.18s ease;
}
.category[open] .category-caret { transform: rotate(180deg); }
.category[open] .category-head {
  border-bottom: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}

/* The whole row is the target, not just the words. */
@media (hover: hover) {
  .category-head:hover h2 { color: color-mix(in oklab, var(--accent) 80%, var(--ink)); }
  .category-head:hover .category-caret { color: var(--ink); }
}

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

.category-note { margin: 12px 16px -2px; color: var(--muted); font-size: 0.9rem; }

.items { list-style: none; margin: 0; padding: 4px 16px 6px; }
/* A row is a picture and everything else, side by side. It is a flex row rather
   than a float, so a line with no picture takes the full width with no rule
   about it and no empty column where one would have been - which is most lines
   on most menus, and the layout has to be right for those first. */
.item {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 12px 0; border-top: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
.item:first-child { border-top: none; }
/* `min-width: 0` and not a default `auto`: a flex child refuses to shrink below
   the widest unbreakable thing in it, and a long dish name would otherwise push
   the price off the side of the phone rather than wrapping. */
.item-body { flex: 1; min-width: 0; }

/* Name on the left, price on the right, and a leader between them - which is
   what makes a column of prices readable without a table. */
.item-head { display: flex; align-items: baseline; gap: 10px; }
.item-name { font-weight: 600; }
.item-head::after {
  content: ""; flex: 1 1 auto; order: 1;
  align-self: end; margin-bottom: 0.32em; height: 1px;
  background: repeating-linear-gradient(
    to right,
    color-mix(in srgb, var(--ink) 22%, transparent) 0 2px,
    transparent 2px 6px
  );
}
.item-price {
  order: 2; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap;
  display: flex; flex-direction: column; align-items: flex-end; line-height: 1.25;
}
/* The conversion, under the price it is a conversion of - quieter and lighter,
   because the venue charges in the currency above it. */
.item-approx {
  font-size: 0.78rem; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.item-note { margin: 3px 0 0; color: var(--muted); font-size: 0.92rem; }

.item-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 7px 0 0; }
/* A mark in its own colour, on a wash of the same colour. `--tag` is set on the
   chip by the page; the fallback is the venue's accent, which is what the
   "sold out" chip and anything without a colour of its own take.
   The colour is pulled 30% toward the venue's ink before it is drawn - see the
   note above `.contact-dot`, which is where that number comes from. */
.tag {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px 3px 8px; border-radius: 999px;
  font-size: 0.75rem; font-weight: 600;
  background: color-mix(in srgb, var(--tag, var(--accent)) 13%, transparent);
  color: color-mix(in oklab, var(--tag, var(--accent)) 70%, var(--ink));
}
.tag .icon { width: 13px; height: 13px; flex: none; }
/* Sold out: greyed and struck through, but still in its place on the list, so
   a guest reading it twice does not wonder whether they misremembered. */
.item.is-out .item-name, .item.is-out .item-price { text-decoration: line-through; }
.item.is-out { opacity: 0.55; }
.tag-out { background: color-mix(in srgb, var(--ink) 12%, transparent); color: var(--muted); }
/* The venue's own badge - "Chef's pick" - filled with the colour it picked
   rather than washed with it, so it reads as the venue speaking and not as one
   more fact about the dish. The lettering is white or near-black, whichever the
   server worked out reads on that colour (`MenuBadge.ink`); a mix toward the
   page's ink, as the marks use, cannot be right for a colour nobody chose in
   advance. The hairline is for the colours that sit close to the page's own -
   a pale yellow on paper, navy on midnight - where the chip would otherwise
   lose its edge. */
.tag-own {
  background: var(--tag); color: var(--tag-ink, #fff);
  font-weight: 700; letter-spacing: 0.01em;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 18%, transparent);
}

/* --- Offers ------------------------------------------------------------------
   A promotion is a small poster, not a priced line: a claim, the terms under
   it, and often a price against another price. So it gets a bounded card of its
   own rather than the dotted leader the items list uses, which reads as "name
   ... price" and has nowhere to put any of that. */
.offers, .events { list-style: none; margin: 22px 0 0; padding: 0; display: grid; gap: 12px; }

.offer {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 4%, var(--surface));
}
.offer-body, .event-body { flex: 1; min-width: 0; }
.offer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.offer-name, .event-name {
  margin: 0; font-size: calc(1rem * var(--heading-scale, 1)); line-height: 1.3;
  font-family: var(--font-heading, inherit); font-weight: var(--heading-weight, 700);
}

/* The chip. In the venue's accent because a promotion is the one thing on the
   page that is meant to be shouted, and the accent is the loudest colour the
   venue has given us. Uppercase, because what goes in it is "1+1" or
   "HAPPY HOUR" and neither has a sentence case. */
.offer-badge {
  flex: none; padding: 3px 9px; border-radius: 999px;
  background: var(--accent); color: var(--surface);
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; white-space: nowrap;
}

.offer-note { margin: 5px 0 0; color: var(--muted); font-size: 0.92rem; }

.offer-price { display: flex; align-items: baseline; gap: 9px; margin: 9px 0 0; }
.offer-now { font-size: 1.15rem; font-weight: 700; font-variant-numeric: tabular-nums; }
/* What it costs the rest of the time. Struck through and quiet - it is there to
   be compared against, not read. */
.offer-was {
  color: var(--muted); font-weight: 600; font-variant-numeric: tabular-nums;
  text-decoration: line-through;
}

.offer-when, .event-time, .event-entry {
  display: flex; align-items: center; gap: 6px;
  margin: 7px 0 0; color: var(--muted); font-size: 0.84rem;
}
.offer-when .icon, .event-time .icon, .event-entry .icon { width: 13px; height: 13px; flex: none; }

/* --- Events ------------------------------------------------------------------
   The date as a block to the left, because a list of what is on is scanned by
   date before it is read by name. A finished event is not here at all - it
   drops off by itself, see `Event.is_showing`. */
.event {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  border-radius: 12px;
}
/* The date and, under it, the poster. One column, so the row is "when and what
   it looks like" on the left and "what it is" on the right - rather than three
   blocks in a line, which on a phone left the name a third of the width.

   The width is the poster's, so the date block above it is exactly as wide: two
   stacked things of different widths read as two things. */
.event-aside {
  flex: none; display: flex; flex-direction: column; gap: 8px;
  width: clamp(72px, 20vw, 100px);
}
/* A calendar page: a strip across the top carrying the month and the year, and
   the day large under it.

   It was three stacked lines on one accent-filled box, which was fine while the
   block was 54px wide and became a lot of air around a two-digit number once it
   was as wide as the poster beneath it. Two grounds fill the width by doing
   something with it: the strip is a band that has to run edge to edge, and the
   number below has room to be the size it deserves.

   Same 10px radius and the same framed-block language as the picture under it,
   so the left-hand column reads as two cards rather than as a chip and a photo. */
.event-date {
  display: flex; flex-direction: column; overflow: hidden;
  border-radius: 10px; text-align: center; line-height: 1.1;
  background: color-mix(in srgb, var(--accent) 11%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
}
.event-when {
  padding: 4px 4px 3px;
  background: var(--accent); color: var(--surface);
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.07em;
  text-transform: uppercase; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* The number, at the size it is worth: a list of what is on is scanned by date
   before it is read by name. In the accent rather than in white on it, because
   it is on the pale ground now - and pulled toward the ink so it clears 4.5:1
   on every one of the five templates, the same mix the diet chips use. */
.event-day {
  padding: 5px 4px 7px;
  font-size: 1.6rem; font-weight: 700; font-variant-numeric: tabular-nums;
  color: color-mix(in oklab, var(--accent) 78%, var(--ink));
}
.event-note { margin: 5px 0 0; color: var(--muted); font-size: 0.92rem; }

/* --- Pictures ----------------------------------------------------------------
   A square on the left of the row, and a magnifier under it that opens the same
   picture over the screen.

   Square, always, and that is a decision about the *list* rather than about any
   one photograph: a column of pictures in whatever shapes their phones produced
   is a ragged left edge, and no amount of CSS turns that back into a column
   somebody can scan. What the crop is squared *around* is the venue's decision,
   made at the moment it uploads - see `menus/js/imagecrop.js`.

   `aspect-ratio` and not a height: the box is the right size and shape before
   the file arrives, so a menu does not jump under the thumb about to tap it. */
.shot {
  flex: none; margin: 0;
  /* No gap: the picture and the button under it are one shape. The picture is
     rounded at the top only and the button at the bottom only, and the picture
     keeps no bottom edge of its own - so the line between them is the button's
     top edge, drawn once, and the two read as a single framed block rather than
     as a photograph with a control parked under it. */
  display: flex; flex-direction: column; align-items: stretch;
  width: 64px;
}
/* An offer and an event are posters rather than lines, and get the bigger
   square. Same component, one number apart - a second way of drawing a picture
   would be a second thing to keep in step for no gain a guest can see.

   `clamp` rather than a number: on a 360px phone a fixed 100px poster and the
   card's own padding leave the claim under it about half the row. The floor is
   what still reads as a picture, the ceiling is what a poster is worth on a
   laptop. An event's poster takes its width from `.event-aside` instead, which
   is set to the same clamp so the date block above it lines up. */
.shot-big { width: clamp(72px, 20vw, 100px); }
.event-aside .shot { width: 100%; }

/* The picture is a target too - see `menus/_shot.html`. `zoom-in` is the cursor
   the platform already uses for exactly this, so on a desktop the affordance
   costs nothing and says itself. */
.shot-frame { display: block; cursor: zoom-in; -webkit-tap-highlight-color: transparent; }
.shot-img {
  display: block; width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: 10px 10px 0 0;
  /* A photograph zoomed out to keep a wide shot whole has transparent corners -
     the crop pads with nothing rather than with a colour, because this page has
     five templates and one of them is near-black. A faint wash of the venue's
     own ink is what shows through, so the square still reads as a square. */
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
  border-bottom: none;
}
/* A picture with nothing under it keeps all four corners - the join only exists
   where there is something to join to. Nothing draws one today; the rule is
   here so that the day something does, it is not a square with two flat
   corners and no explanation. */
.shot:not(:has(.shot-zoom)) .shot-img {
  border-radius: 10px; border-bottom: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
}

/* The magnifier. Under the picture rather than over a corner of it: a menu is
   read at arm's length in poor light, and a 20px overlay on a 64px photograph
   is both hard to hit and in the way of the thing it is about. Full width of
   the square, so the target is the whole of what the eye already thinks of as
   the picture's column. */
.shot-zoom {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 26px; padding: 3px;
  border-radius: 0 0 10px 10px;
  cursor: pointer;
  background: color-mix(in srgb, var(--accent) 11%, transparent);
  color: color-mix(in oklab, var(--accent) 78%, var(--ink));
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  /* A label is not a button, and a tap on it must not select the word that is
     not there. */
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s ease, transform 0.15s ease;
}
.shot-zoom .icon { width: 15px; height: 15px; }
@media (hover: hover) {
  .shot-zoom:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
}
.shot-zoom:active { transform: scale(0.96); }
.zoom-input:focus-visible + .shot .shot-zoom { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) {
  /* Pointing at the picture lights the button under it, so the two read as the
     one control they are. */
  .shot-frame:hover + .shot-zoom { background: color-mix(in srgb, var(--accent) 20%, transparent); }
}

/* A sold-out dish greys out; its picture goes with the rest of the row rather
   than staying the one bright thing on it. */
.item.is-out .shot-img { filter: grayscale(0.7); }

/* --- The enlarged picture ---------------------------------------------------
   A checkbox and this rule are the whole lightbox: no script, no request, and a
   tap works exactly as a click does. The input is off-screen rather than
   `display: none`, so it keeps its place in the keyboard's order - the same
   arrangement the tabs at the top of this page use.

   Above the currency badge, which is the only other fixed thing here. */
.zoom-input {
  position: absolute; width: 1px; height: 1px; opacity: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.zoom { display: none; }
/* `:has` on the row rather than `~` from the checkbox, and that is not a
   flourish. An event's picture is stacked under its date inside `.event-aside`,
   which puts the checkbox one level down while the panel stays a child of the
   row - and a sibling combinator stops matching the moment either of them is
   wrapped in anything. Asking the row whether something inside it is ticked
   survives any arrangement of the markup above it. The tab switch at the top of
   this file leans on `:has` the same way. */
.menu li:has(.zoom-input:checked) .zoom {
  display: flex; align-items: center; justify-content: center;
  position: fixed; inset: 0; z-index: 100; padding: 18px;
}

/* The darkened ground, and the second way out. On a phone this is the one
   people actually use - a thumb lands anywhere but the corner - so it is a
   label for the same checkbox rather than decoration.

   `touch-action: none` keeps a drag on it from scrolling the menu underneath;
   the script adds the same for the page as a whole, and this is the half that
   works without it. */
.zoom-back {
  position: absolute; inset: 0; cursor: pointer;
  background: rgb(0 0 0 / 0.78);
  touch-action: none;
}

/* Never past 512, because that is the largest square that is ever stored and a
   picture blown up past its own resolution is a blurred picture. Under that, as
   much of the window as leaves the ground visible around it - which is what
   says "this closes" without a word. */
.zoom-card {
  position: relative; margin: 0;
  /* Three ceilings, and the third is the one that is easy to forget. 512 is the
     largest square that is ever stored, and a picture blown up past its own
     resolution is a blurred picture. 92vw leaves the darkened ground visible
     around it, which is what says "this closes" without a word. And `74dvh` is
     the phone held sideways: without it a square 92vw wide is taller than the
     screen, so the caption and the close mark are below the fold of a panel
     that has no scrollbar. `dvh` rather than `vh` because the browser's own
     bars are counted into `vh`. */
  width: min(92vw, 512px, 74dvh); max-width: 100%;
}
.zoom-card img {
  display: block; width: 100%; aspect-ratio: 1; object-fit: contain;
  border-radius: 14px;
  /* The thumbnail, set as this element's background by the page. It is already
     in the cache, so there is a picture here the instant it opens while the
     full square is still arriving - and it is painted over the moment it does. */
  background-size: cover; background-position: center;
  box-shadow: 0 24px 60px rgb(0 0 0 / 0.5);
}

/* The close mark, on the picture rather than beside it - there is nothing else
   on this screen for it to be beside. 40px because it is a target for a thumb,
   and filled rather than outlined so it is legible over a photograph of
   anything. */
.zoom-close {
  position: absolute; top: 8px; right: 8px;
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  cursor: pointer;
  background: rgb(0 0 0 / 0.55); color: #fff;
  border: 1px solid rgb(255 255 255 / 0.35);
  backdrop-filter: blur(6px);
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s ease, transform 0.15s ease;
}
.zoom-close .icon { width: 17px; height: 17px; }
@media (hover: hover) {
  .zoom-close:hover { background: rgb(0 0 0 / 0.78); }
}
.zoom-close:active { transform: scale(0.94); }
.zoom-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* Which dish it is. Over the darkened ground rather than in the venue's
   colours: this panel is not part of the menu's surface and a chip of the
   template's paper here would read as a piece of the page that had come loose. */
.zoom-name {
  margin: 12px 0 0; text-align: center;
  color: #fff; font-size: 0.95rem; font-weight: 600;
  text-shadow: 0 1px 6px rgb(0 0 0 / 0.6);
}

/* Set on <html> by `menus/js/lightbox.js` while a picture is open. Without the
   script the ground still swallows a drag; this is the half that keeps the page
   itself from moving, so closing the picture leaves the guest where they were
   in a forty-line menu. */
.is-zoomed body { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .shot-zoom, .zoom-close { transition: none; }
}

/* What a tab says when there is nothing in it yet. An events tab with no
   upcoming events is the ordinary state of one between concerts, and a blank
   panel there reads as the page having failed to load.

   Quiet on purpose. This is not an error and not a call to action - the guest
   can do nothing about it - so it is a mark and a sentence with air around
   them, and no dashed "drop something here" box, which is an editor's idiom
   showing through onto somebody's menu. */
.panel-empty {
  margin: 34px 0; padding: 8px 16px 12px;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  text-align: center;
}

/* The tab's own glyph, drawn large and faint - a disc of the venue's accent at
   a strength that reads as absence rather than as a button. */
.panel-empty-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 58px; height: 58px; border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: color-mix(in oklab, var(--accent) 60%, var(--muted));
}
.panel-empty-mark .icon { width: 24px; height: 24px; }

.panel-empty p {
  margin: 0; max-width: 32ch;
  color: var(--muted); font-size: 0.95rem; line-height: 1.5;
}

/* --- The currency badge -----------------------------------------------------
   Fixed to the lower right so it travels with the guest down a long menu, and
   clear of the thumb's resting place on a phone. A <details>, so it opens and
   closes with no script; only the conversion needs one. */
.fx { position: fixed; right: 14px; bottom: 14px; z-index: 20; }
/* A pill that says what it is for. It is the one control on the page, it has to
   be found on a phone at a table and on a desktop across a wide window, and a
   circle with three letters in it is neither. */
.fx-button {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 16px; border-radius: 999px; cursor: pointer;
  background: var(--accent); color: var(--surface);
  font-size: 0.9rem; font-weight: 650; white-space: nowrap;
  box-shadow: 0 8px 22px rgb(0 0 0 / 0.28);
  list-style: none;
}
.fx-button::-webkit-details-marker { display: none; }
.fx-icon { width: 17px; height: 17px; flex: none; }
.fx-code {
  padding: 2px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--surface) 24%, transparent);
  font-variant-numeric: tabular-nums;
}
/* On a narrow phone the words give way and the pill keeps its icon and the code
   - which is still more than the bare circle said. */
@media (max-width: 420px) {
  .fx-text { display: none; }
  .fx-button { padding: 12px 14px; }
}
.fx-menu {
  position: absolute; right: 0; bottom: 66px; min-width: 190px;
  padding: 10px; border-radius: 14px;
  background: var(--surface); color: var(--ink);
  border: 1px solid color-mix(in srgb, var(--ink) 14%, transparent);
  box-shadow: 0 12px 30px rgb(0 0 0 / 0.20);
}
.fx-note { margin: 2px 4px 8px; font-size: 0.75rem; color: var(--muted); }
.fx-option {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 9px 10px; margin: 0; border: none; border-radius: 9px;
  background: none; color: inherit; cursor: pointer;
  font: inherit; font-weight: 600; text-align: left;
}
.fx-option:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.fx-option.is-active { background: var(--accent); color: var(--surface); }
.fx-main { font-size: 0.72rem; font-weight: 600; opacity: 0.7; }

/* --- The notice ---------------------------------------------------------------
   One line across the very top in the venue's accent - "Live music tonight".
   The accent and the page's own colour are the pair the offer chip already
   uses, so whatever the preset, the line is as readable as that chip is. A
   band of colour of its own rather than the page's surface, so it does not
   cover the texture with a plain strip either. The megaphone is inline, in
   front of the first word, so a notice that wraps on a phone keeps it beside
   its text rather than out at the edge of the band. */
.announce {
  margin: 0; padding: 10px 16px;
  background: var(--accent); color: var(--surface);
  font-size: 0.9rem; font-weight: 650; line-height: 1.35; text-align: center;
}
.announce .icon { width: 16px; height: 16px; margin-right: 8px; vertical-align: -3px; }

/* --- The story ----------------------------------------------------------------
   A few lines about the place under the menu, centred in the menu's column,
   with the portrait in a ring of the page's own colour so a photograph with a
   white background still has an edge. No ground of its own - the texture runs
   behind it as it runs behind everything else. */
.story { max-width: 620px; margin: 40px auto 0; padding: 0 20px; text-align: center; }
.story-portrait {
  display: block; width: 132px; height: 132px; margin: 0 auto 16px;
  border-radius: 50%; object-fit: cover;
  box-shadow: 0 0 0 4px var(--surface), 0 0 0 5px color-mix(in srgb, var(--ink) 16%, transparent);
}
.story-title {
  margin: 0 0 10px; font-size: calc(1.05rem * var(--heading-scale, 1));
  font-family: var(--font-heading, inherit); font-weight: var(--heading-weight, 700);
  letter-spacing: var(--heading-tracking, 0.06em);
  text-transform: var(--heading-case, uppercase); color: var(--accent);
  line-height: 1.25;
}
/* Held to a reading measure: centred lines of eighty characters lose the eye
   on the way back to a ragged left edge. */
.story-text { max-width: 34em; margin-inline: auto; font-size: 0.98rem; line-height: 1.6; }
.story-text p { margin: 0 0 10px; }
.story-text p:last-child { margin-bottom: 0; }

/* --- The foot ---------------------------------------------------------------
   Where to find the place, once somebody has decided to. Icons only, in one
   row: labelled links would take five lines of a page that is read on a phone,
   and every one of these is a shape people already recognise. */
.venue-foot {
  margin-top: 34px; padding: 22px 20px 8px;
  text-align: center; color: var(--muted); font-size: 0.82rem;
  border-top: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
}
.venue-contact {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;
  margin: 0 0 14px;
}
/* Each network in its own colour - everyone knows these before they know the
   shapes. `--net` is set on the button by the page; anything without a colour of
   its own (X, and the phone, mail and website buttons) takes the menu's ink and
   so follows the template it is on.
   The brand colour is pulled 30% toward the venue's ink before it is drawn,
   because a brand colour is chosen to be a plate with a white mark on it, not a
   mark on somebody else's page: WhatsApp's green measured 1.88:1 on the light
   templates and Telegram's blue 2.6:1, which is a logo you have to look for. The
   mix is toward the *ink* rather than toward black on purpose - on the dark
   template the ink is near-white, so the same rule lightens the too-dark ones
   (LinkedIn, the nut-free brown) instead of burying them further. 70% is the
   least that clears 4.5:1 for the chip text and 3:1 for the icons across all
   five templates, measured; at that mix every colour is still its own. */
.contact-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--net, var(--ink)) 28%, transparent);
  color: color-mix(in oklab, var(--net, var(--ink)) 70%, var(--ink));
  text-decoration: none;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.contact-dot .icon { width: 18px; height: 18px; }
/* Hover tints the ground and lifts the button; the glyph keeps its colour. The
   first version filled the circle with the accent and set the glyph to the
   surface colour, which on a menu whose accent *is* its ink drew the icon in
   its own background - a button that vanished as soon as it was pointed at. */
.contact-dot:hover, .contact-dot:focus-visible {
  background: color-mix(in srgb, var(--net, var(--ink)) 14%, transparent);
  border-color: var(--net, var(--ink));
  transform: translateY(-2px);
}
.contact-dot:focus-visible { outline: 2px solid var(--net, var(--ink)); outline-offset: 2px; }
.venue-foot-name {
  margin: 0; color: var(--ink);
  font-family: var(--font-heading, inherit); font-weight: var(--heading-weight, 600);
}
.venue-foot-hours { margin: 4px 0 0; }

/* --- Wider screens ---------------------------------------------------------
   The menu keeps its column rather than stretching: a price two feet from its
   dish is a price nobody can read off the row it belongs to. */
@media (min-width: 700px) {
  body { font-size: 18px; }
  .venue { padding-top: 44px; }
  .venue h1 { font-size: calc(2rem * var(--heading-scale, 1)); }
  /* The cover stops being edge to edge once the page is wider than the menu's
     column by much: a 5:2 band across a whole laptop is a wall, not a header. */
  .venue-cover {
    width: min(100% + 40px, 980px); margin: 0 auto 22px;
    border-radius: 0 0 20px 20px;
  }
  .has-cover .venue-logo { margin-top: -66px; }
  /* The column does not stretch - see above - but there is room for a slightly
     larger picture in it once the page is not a phone. */
  .shot { width: 76px; }
}

/* --- Layouts -------------------------------------------------------------------
   `data-layout` on <body>, from `menus.styles.LAYOUTS`. "blocks" is everything
   above - a card per heading that opens - and needs nothing here. The other
   three print their headings open as <section>s (see public.html), so they
   start by taking the card away and differ in what they put in its place. */
[data-layout="classic"] .category,
[data-layout="compact"] .category,
[data-layout="grid"] .category {
  border: none; border-radius: 0; background: none; overflow: visible;
}
div.category-head { cursor: auto; }

/* Classic: a printed menu. The heading centred over a thin rule with a dot in
   the middle of it, the dishes under it with their dotted leaders, and air
   between one heading and the next instead of a box around each. */
[data-layout="classic"] .category { margin: 30px 0 6px; }
[data-layout="classic"] .category-head {
  flex-direction: column; gap: 10px; padding: 0 0 4px; text-align: center;
}
[data-layout="classic"] .category h2 { flex: none; }
[data-layout="classic"] .category-head::after {
  content: ""; width: 132px; height: 9px;
  background:
    linear-gradient(var(--accent), var(--accent)) left center / 54px 1px no-repeat,
    linear-gradient(var(--accent), var(--accent)) right center / 54px 1px no-repeat,
    radial-gradient(circle, var(--accent) 0 3px, transparent 3.5px) center / 9px 9px no-repeat;
  opacity: 0.7;
}
[data-layout="classic"] .category-note { margin: 6px 0 0; text-align: center; font-style: italic; }
[data-layout="classic"] .items { padding: 6px 0 0; }
[data-layout="classic"] .item { border-top: none; padding: 10px 0; }

/* Compact: a bar's list. A rule in the accent under each heading, tight rows,
   small pictures - forty drinks scanned rather than read. Nothing goes under
   the 15px floor the rest of the page keeps. */
[data-layout="compact"] .category { margin: 22px 0 0; }
[data-layout="compact"] .category-head {
  padding: 0 0 6px; border-bottom: 2px solid var(--accent);
}
[data-layout="compact"] .category-note { margin: 8px 0 0; }
[data-layout="compact"] .items { padding: 2px 0 0; }
[data-layout="compact"] .item { padding: 8px 0; gap: 10px; }
[data-layout="compact"] .item-note { margin-top: 1px; font-size: 0.9rem; }
[data-layout="compact"] .item-tags { margin-top: 4px; }
[data-layout="compact"] .item .shot { width: 48px; }
[data-layout="compact"] .shot-zoom { min-height: 22px; }

/* Grid: two columns of cards with the picture on top, for a kitchen whose food
   sells itself. A card without a picture is simply shorter. The leader goes -
   there is no row for it to run along - and the price sits under the name. */
[data-layout="grid"] .category { margin: 24px 0 0; }
[data-layout="grid"] .category-head { padding: 0 2px 10px; }
[data-layout="grid"] .category-note { margin: -4px 2px 12px; }
[data-layout="grid"] .items {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
  padding: 0;
  /* Not the grid's default `stretch`, which pulls a card without a picture down
     to the one beside it and leaves it half empty. */
  align-items: start;
}
[data-layout="grid"] .item,
[data-layout="grid"] .item:first-child {
  flex-direction: column; gap: 0; padding: 0;
  border: 1px solid color-mix(in srgb, var(--ink) 11%, transparent);
  border-radius: 14px; overflow: hidden;
  background: color-mix(in srgb, var(--ink) 3.5%, var(--surface));
}
[data-layout="grid"] .item .shot { width: 100%; }
[data-layout="grid"] .item .shot-img { border: none; border-radius: 0; }
[data-layout="grid"] .item .shot-zoom {
  border-radius: 0; border-left: none; border-right: none;
}
[data-layout="grid"] .item-body { width: 100%; padding: 10px 12px 12px; }
[data-layout="grid"] .item-head { flex-direction: column; align-items: flex-start; gap: 2px; }
[data-layout="grid"] .item-head::after { display: none; }
[data-layout="grid"] .item-price { align-items: flex-start; }

/* --- Textures ------------------------------------------------------------------
   `data-texture` on <body>, from `menus.styles.TEXTURES`. One layer behind
   everything, filled with the venue's own ink and cut into a pattern by a
   mask - so the grain is dark on a light page and light on a dark one with no
   second set of rules. Its opacity is `--texture-strength`, which is never more
   ink than an open heading's card already lays under its dishes (6%), so no
   texture makes a preset harder to read than its open card already is -
   `test_the_look.py` holds every texture to it. That is also why the cards
   above are opaque - a translucent card on a texture would be the two tints
   added together.

   Patterns are SVG noise for the organic ones and gradients for the regular
   ones. The SVG is a data: URL, which the page's Content-Security-Policy allows
   for images, and a mask is fetched as one. Its filter region is the tile
   itself (x/y 0, 100% by 100%): the default reaches 10% past every edge, and
   `stitchTiles` then stitches that larger square, so the 480px chalk showed a
   straight seam at every repeat. */
body:not([data-texture="none"]) { position: relative; isolation: isolate; min-height: 100vh; }
/* The tab block paints the surface for the bar to stick over the list, and it
   wraps every panel - so on a textured page it was a plain slab from the tabs
   to the footer, with the grain only in the margins around it. */
body:not([data-texture="none"]) .tabs { background: none; }
body:not([data-texture="none"])::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: var(--ink); opacity: var(--texture-strength, 0);
}
body[data-texture="paper"]::before {
  /* Fine fibre: high-frequency noise, its red channel turned into coverage. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2.2 0 0 0 -.75'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E") 0 0 / 220px 220px repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2.2 0 0 0 -.75'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E") 0 0 / 220px 220px repeat;
}
body[data-texture="chalk"]::before {
  /* Smudges: low-frequency noise, stretched sideways the way a wiped board is. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.006 .03' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2.6 0 0 0 -1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E") 0 0 / 480px 480px repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.006 .03' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2.6 0 0 0 -1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E") 0 0 / 480px 480px repeat;
}
body[data-texture="linen"]::before {
  /* A weave: fine threads both ways, the weft a little fainter than the warp. */
  -webkit-mask:
    repeating-linear-gradient(0deg, #000 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgb(0 0 0 / 0.6) 0 1px, transparent 1px 3px);
  mask:
    repeating-linear-gradient(0deg, #000 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgb(0 0 0 / 0.6) 0 1px, transparent 1px 3px);
}
body[data-texture="dots"]::before {
  -webkit-mask: radial-gradient(circle, #000 0 1.3px, transparent 1.8px) 0 0 / 14px 14px repeat;
  mask: radial-gradient(circle, #000 0 1.3px, transparent 1.8px) 0 0 / 14px 14px repeat;
}
body[data-texture="stripes"]::before {
  -webkit-mask: repeating-linear-gradient(135deg, #000 0 2px, transparent 2px 12px);
  mask: repeating-linear-gradient(135deg, #000 0 2px, transparent 2px 12px);
}

/* --- When a menu is not being served ----------------------------------------
   The venue's own colours, its name, and one sentence. Nothing about billing,
   and nothing for a guest to do except ask - which is why the address is a
   link they can tap rather than a line of text to copy out. */
.notice { max-width: 460px; margin: 0 auto; padding: 20px 24px 60px; text-align: center; }
.notice-lead { margin: 0 0 10px; font-size: 1.1rem; font-weight: 650; }
.notice-note { margin: 0; color: var(--muted); font-size: 0.95rem; line-height: 1.5; }
.notice-note a { color: var(--accent); font-weight: 600; }
