/* ── Palate design tokens ───────────────────────────────────────────────────
   Ground: bone with a mulberry bias. Accent: mulberry (wine list, not terracotta).
   The like/dislike axis is deliberately a separate hue family from the accent so
   a strong opinion never reads as branding.
   ──────────────────────────────────────────────────────────────────────────── */
:root {
  --bg: #F3F0F2;
  --surface: #FCFAFB;
  --surface-2: #F7F3F5;
  --ink: #1A1418;
  --ink-2: #4B3F46;
  --muted: #7A6A73;
  --line: #E2D9DE;
  --line-strong: #CFC2C9;

  --accent: #7A2E4E;
  --accent-ink: #FFFFFF;
  --accent-soft: #F1E2E8;
  --accent-line: #DFC3CF;

  --good: #2F6E52;
  --good-soft: #E1EEE7;
  --bad: #A9453C;
  --bad-soft: #F6E2DF;
  --warn: #9A6410;
  --warn-soft: #F7EBD8;

  --shadow: 0 1px 2px rgba(26, 20, 24, .05), 0 8px 24px -12px rgba(26, 20, 24, .18);
  --radius: 10px;
  --radius-sm: 6px;

  --display: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", "URW Palladio L", Georgia, serif;
  --body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;

  /* ── Typographic & layout scale ───────────────────────────────────────────
     Everything a theme (data-skin) is allowed to move. Component rules read these
     rather than hard numbers, so a new theme is a block of variables and not a fork
     of the stylesheet. Colour is deliberately NOT in here — that is the palette's job,
     and keeping the two apart is what lets any theme run in any colour.
     ────────────────────────────────────────────────────────────────────────── */
  --base-size: 15px;
  --base-leading: 1.55;
  --h1-size: 1.9rem;
  --h2-size: 1.35rem;
  --h3-size: 1.05rem;
  --head-weight: 600;
  --head-spacing: -.01em;
  --head-transform: none;
  --head-family: var(--display);

  --eyebrow-size: 10px;
  --eyebrow-spacing: .13em;
  --eyebrow-transform: uppercase;
  --eyebrow-weight: 600;

  --rail-w: 232px;
  --main-pad-y: 26px;
  --main-pad-x: 30px;
  --card-pad: 16px;
  --stack-gap: 14px;
  --border-w: 1px;
  --wordmark-size: 22px;
  --wordmark-spacing: -.02em;
  --nav-size: 14px;
  --nav-transform: none;
  --btn-weight: 500;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #141013; --surface: #1C171B; --surface-2: #241D23;
    --ink: #F1E9ED; --ink-2: #CBB9C3; --muted: #9C8A94;
    --line: #322A30; --line-strong: #463B43;
    --accent: #D9789E; --accent-ink: #21121A; --accent-soft: #33202A; --accent-line: #4C3340;
    --good: #74C09A; --good-soft: #1D2E26; --bad: #E38C82; --bad-soft: #33201E;
    --warn: #D6A44E; --warn-soft: #302517;
    --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 28px -14px rgba(0,0,0,.7);
  }
}
/* NATIVE CONTROLS FOLLOW THE PAGE (2026-09-17). A date field's value, its calendar popover and a
   select's dropdown are drawn by the browser, not by these tokens, and without `color-scheme`
   iOS drew the birthday in black on a night-mode card: the field looked empty. The explicit
   themes set it too, so a device in light mode showing Palate at Night gets dark controls. */
:root { color-scheme: light; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { color-scheme: dark; } }
:root[data-theme="light"] { color-scheme: light; }

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #141013; --surface: #1C171B; --surface-2: #241D23;
  --ink: #F1E9ED; --ink-2: #CBB9C3; --muted: #9C8A94;
  --line: #322A30; --line-strong: #463B43;
  --accent: #D9789E; --accent-ink: #21121A; --accent-soft: #33202A; --accent-line: #4C3340;
  --good: #74C09A; --good-soft: #1D2E26; --bad: #E38C82; --bad-soft: #33201E;
  --warn: #D6A44E; --warn-soft: #302517;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 28px -14px rgba(0,0,0,.7);
}
:root[data-theme="light"] {
  --bg: #F3F0F2; --surface: #FCFAFB; --surface-2: #F7F3F5;
  --ink: #1A1418; --ink-2: #4B3F46; --muted: #7A6A73;
  --line: #E2D9DE; --line-strong: #CFC2C9;
  --accent: #7A2E4E; --accent-ink: #FFFFFF; --accent-soft: #F1E2E8; --accent-line: #DFC3CF;
  --good: #2F6E52; --good-soft: #E1EEE7; --bad: #A9453C; --bad-soft: #F6E2DF;
  --warn: #9A6410; --warn-soft: #F7EBD8;
  --shadow: 0 1px 2px rgba(26,20,24,.05), 0 8px 24px -12px rgba(26,20,24,.18);
}

/* ── Palettes ────────────────────────────────────────────────────────────────
   Named accent palettes on top of the light/dark theme. Each redefines only the
   accent family and the stripe tint; the ground stays with the theme. Selected
   via data-palette on <html>, remembered in localStorage.
   ──────────────────────────────────────────────────────────────────────────── */
:root { --stripe: #EDE7EA; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --stripe: #211B20; } }
:root[data-theme="dark"] { --stripe: #211B20; }
:root[data-theme="light"] { --stripe: #EDE7EA; }

:root[data-palette="bistro"] {
  --accent: #8A4A17; --accent-ink: #FFF; --accent-soft: #F3E5D8; --accent-line: #E2C9AE;
  --stripe: #EFE8E0;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="bistro"]:not([data-theme="light"]) {
    --accent: #E09B58; --accent-ink: #221304; --accent-soft: #33261A; --accent-line: #4C3A26;
    --stripe: #241E17;
  }
}
:root[data-palette="bistro"][data-theme="dark"] {
  --accent: #E09B58; --accent-ink: #221304; --accent-soft: #33261A; --accent-line: #4C3A26;
  --stripe: #241E17;
}

:root[data-palette="harbor"] {
  --accent: #2C5F7C; --accent-ink: #FFF; --accent-soft: #DFEAF1; --accent-line: #BAD2E0;
  --stripe: #E7ECEF;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="harbor"]:not([data-theme="light"]) {
    --accent: #7FB5D3; --accent-ink: #0C1E28; --accent-soft: #1D2C36; --accent-line: #2C4250;
    --stripe: #1A2228;
  }
}
:root[data-palette="harbor"][data-theme="dark"] {
  --accent: #7FB5D3; --accent-ink: #0C1E28; --accent-soft: #1D2C36; --accent-line: #2C4250;
  --stripe: #1A2228;
}

:root[data-palette="verdant"] {
  --accent: #2F6E52; --accent-ink: #FFF; --accent-soft: #E1EEE7; --accent-line: #BFD9CC;
  --stripe: #E8EDEA;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="verdant"]:not([data-theme="light"]) {
    --accent: #74C09A; --accent-ink: #0D211A; --accent-soft: #1D2E26; --accent-line: #2C453A;
    --stripe: #1A211E;
  }
}
:root[data-palette="verdant"][data-theme="dark"] {
  --accent: #74C09A; --accent-ink: #0D211A; --accent-soft: #1D2E26; --accent-line: #2C453A;
  --stripe: #1A211E;
}

:root[data-palette="ember"] {
  --accent: #A8391F; --accent-ink: #FFF; --accent-soft: #F6E1DB; --accent-line: #E5BFB3;
  --stripe: #EFE6E3;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="ember"]:not([data-theme="light"]) {
    --accent: #EE9077; --accent-ink: #2A0F07; --accent-soft: #37211A; --accent-line: #4F3227;
    --stripe: #241B18;
  }
}
:root[data-palette="ember"][data-theme="dark"] {
  --accent: #EE9077; --accent-ink: #2A0F07; --accent-soft: #37211A; --accent-line: #4F3227;
  --stripe: #241B18;
}

:root[data-palette="indigo"] {
  --accent: #3B3B8F; --accent-ink: #FFF; --accent-soft: #E3E3F4; --accent-line: #C2C2E4;
  --stripe: #E9E9F0;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="indigo"]:not([data-theme="light"]) {
    --accent: #9A9AE8; --accent-ink: #12123A; --accent-soft: #23234A; --accent-line: #35356B;
    --stripe: #1B1B27;
  }
}
:root[data-palette="indigo"][data-theme="dark"] {
  --accent: #9A9AE8; --accent-ink: #12123A; --accent-soft: #23234A; --accent-line: #35356B;
  --stripe: #1B1B27;
}

:root[data-palette="saffron"] {
  --accent: #916A0B; --accent-ink: #FFF; --accent-soft: #F6EDD5; --accent-line: #E3D2A5;
  --stripe: #EFEBDF;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="saffron"]:not([data-theme="light"]) {
    --accent: #DCBB5C; --accent-ink: #241B03; --accent-soft: #332B16; --accent-line: #4B4024;
    --stripe: #232017;
  }
}
:root[data-palette="saffron"][data-theme="dark"] {
  --accent: #DCBB5C; --accent-ink: #241B03; --accent-soft: #332B16; --accent-line: #4B4024;
  --stripe: #232017;
}

:root[data-palette="slate"] {
  --accent: #4A5259; --accent-ink: #FFF; --accent-soft: #E7EAEC; --accent-line: #C8CFD3;
  --stripe: #EBEDEE;
}
@media (prefers-color-scheme: dark) {
  :root[data-palette="slate"]:not([data-theme="light"]) {
    --accent: #A9B6BF; --accent-ink: #171C20; --accent-soft: #262C31; --accent-line: #3A434A;
    --stripe: #1D2124;
  }
}
:root[data-palette="slate"][data-theme="dark"] {
  --accent: #A9B6BF; --accent-ink: #171C20; --accent-soft: #262C31; --accent-line: #3A434A;
  --stripe: #1D2124;
}

/* ── Themes (data-skin) ──────────────────────────────────────────────────────
   Typography and layout only. Each block moves the scale variables declared in :root and
   nothing else, which is why any theme composes with any palette and with either mode.

   System font stacks throughout — no web fonts. A page that only looks right when a font
   CDN is reachable is a page that breaks offline, and the single-file build is meant to
   run from a thumb drive. The stacks below name genuinely different faces per platform
   rather than pretending one family is installed everywhere, and every one ends in a
   generic family so an unknown platform still lands somewhere sensible.

   "editorial" is the default and matches the :root values, so it needs no block.
   ──────────────────────────────────────────────────────────────────────────── */

/* Diner — big, friendly, rounded. Reads at arm's length on a phone at a table. */
:root[data-skin="diner"] {
  --display: "SF Pro Rounded", "Nunito", "Varela Round", "Trebuchet MS", "Segoe UI", system-ui, sans-serif;
  --body: "SF Pro Rounded", "Nunito", "Segoe UI", system-ui, -apple-system, sans-serif;
  --head-family: var(--display);
  --base-size: 16px;
  --base-leading: 1.6;
  --h1-size: 2.15rem;
  --h2-size: 1.5rem;
  --h3-size: 1.12rem;
  --head-weight: 700;
  --head-spacing: -.015em;
  --radius: 18px;
  --radius-sm: 12px;
  --card-pad: 19px;
  --stack-gap: 16px;
  --rail-w: 244px;
  --nav-size: 15px;
  --eyebrow-spacing: .1em;
  --btn-weight: 700;
  --wordmark-size: 25px;
}

/* Market — dense grotesk. More rows on screen, numbers doing the talking. */
:root[data-skin="market"] {
  --display: Inter, "Segoe UI", Roboto, "Helvetica Neue", system-ui, sans-serif;
  --body: Inter, "Segoe UI", Roboto, "Helvetica Neue", system-ui, sans-serif;
  --head-family: var(--display);
  --base-size: 13.5px;
  --base-leading: 1.45;
  --h1-size: 1.45rem;
  --h2-size: 1.1rem;
  --h3-size: .96rem;
  --head-weight: 650;
  --head-spacing: -.02em;
  --radius: 5px;
  --radius-sm: 3px;
  --card-pad: 12px;
  --stack-gap: 10px;
  --main-pad-y: 18px;
  --main-pad-x: 20px;
  --rail-w: 208px;
  --nav-size: 13px;
  --eyebrow-size: 9.5px;
  --eyebrow-spacing: .09em;
  --wordmark-size: 19px;
  --shadow: 0 1px 1px rgba(26, 20, 24, .04);
}
:root[data-skin="market"] th,
:root[data-skin="market"] td { padding: 5px 8px; }

/* Supper — display serif, wide tracking, a lot of air. The tasting-menu one. */
:root[data-skin="supper"] {
  --display: Didot, "Bodoni MT", "Playfair Display", "Hoefler Text", Garamond, "Times New Roman", serif;
  --body: "Optima", Candara, "Gill Sans", "Gill Sans MT", "Segoe UI", system-ui, sans-serif;
  --head-family: var(--display);
  --base-size: 15.5px;
  --base-leading: 1.7;
  --h1-size: 2.5rem;
  --h2-size: 1.6rem;
  --h3-size: 1.1rem;
  --head-weight: 500;
  --head-spacing: .005em;
  --radius: 2px;
  --radius-sm: 2px;
  --card-pad: 24px;
  --stack-gap: 20px;
  --main-pad-y: 40px;
  --main-pad-x: 44px;
  --rail-w: 250px;
  --eyebrow-spacing: .26em;
  --eyebrow-size: 9.5px;
  --wordmark-size: 26px;
  --wordmark-spacing: .02em;
  --shadow: none;
}

/* Zine — monospace, hard borders, no shadows, shouty headings. */
:root[data-skin="zine"] {
  --display: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;
  --body: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;
  --head-family: var(--display);
  --base-size: 13.5px;
  --base-leading: 1.5;
  --h1-size: 1.6rem;
  --h2-size: 1.2rem;
  --h3-size: 1rem;
  --head-weight: 700;
  --head-spacing: -.03em;
  --head-transform: uppercase;
  --radius: 0px;
  --radius-sm: 0px;
  --border-w: 2px;
  --card-pad: 14px;
  --rail-w: 220px;
  --nav-transform: uppercase;
  --nav-size: 12.5px;
  --eyebrow-spacing: .2em;
  --btn-weight: 700;
  --wordmark-size: 20px;
  --wordmark-spacing: -.04em;
  --shadow: none;
}
:root[data-skin="zine"] .btn { border-width: 2px; text-transform: uppercase; letter-spacing: .04em; }
:root[data-skin="zine"] .chip { border-radius: 0; border-width: 2px; }
:root[data-skin="zine"] .nav a.on { outline: 2px solid var(--accent); outline-offset: -2px; }

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: var(--base-size)/var(--base-leading) var(--body);
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 {
  font-family: var(--head-family); font-weight: var(--head-weight); text-wrap: balance;
  margin: 0; letter-spacing: var(--head-spacing); text-transform: var(--head-transform);
}
h1 { font-size: var(--h1-size); line-height: 1.15; }
h2 { font-size: var(--h2-size); }
h3 { font-size: var(--h3-size); }
p { margin: 0; }
a { color: inherit; }
button, input, select, textarea { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.eyebrow {
  font: var(--eyebrow-weight) var(--eyebrow-size)/1 var(--body);
  letter-spacing: var(--eyebrow-spacing); text-transform: var(--eyebrow-transform); color: var(--muted);
}
.muted { color: var(--muted); }
.small { font-size: 13px; }
.tiny { font-size: 11.5px; }

/* ── Shell ──────────────────────────────────────────────────────────────── */
.shell { display: grid; grid-template-columns: var(--rail-w) minmax(0, 1fr); min-height: 100vh; }
.rail {
  border-right: var(--border-w) solid var(--line); background: var(--surface);
  padding: 20px 14px; display: flex; flex-direction: column; gap: 20px;
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.wordmark { display: flex; align-items: baseline; gap: 8px; padding: 0 8px; }
.wordmark b {
  font-family: var(--head-family); font-size: var(--wordmark-size);
  font-weight: var(--head-weight); letter-spacing: var(--wordmark-spacing);
}
.wordmark span { font: 600 9px/1 var(--body); letter-spacing: .14em; text-transform: uppercase; color: var(--accent); }

.nav { display: flex; flex-direction: column; gap: 1px; }
.nav a {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border-radius: var(--radius-sm); text-decoration: none;
  color: var(--ink-2); font-size: var(--nav-size); text-transform: var(--nav-transform);
  transition: background .12s, color .12s;
}
.nav a:hover { background: var(--surface-2); color: var(--ink); }
.nav a.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.nav a .n { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.nav a.on .n { color: var(--accent); }
.nav-group { display: flex; flex-direction: column; gap: 6px; }

.main {
  /* Named so the checked-in bar can add its own height to whatever this is, at either
     breakpoint, without repeating the number. */
  --main-pad-bottom: 90px;
  min-width: 0; padding: var(--main-pad-y) var(--main-pad-x) var(--main-pad-bottom);
  max-width: 1180px;
}
.page-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 14px; margin-bottom: 20px; }
.page-head .lede { color: var(--muted); font-size: 14px; max-width: 62ch; margin-top: 5px; }

.tabbar { display: none; }

@media (max-width: 860px) {
  .shell { grid-template-columns: 1fr; }
  .rail { display: none; }
  .main { --main-pad-bottom: 84px; padding: 16px 14px var(--main-pad-bottom); }
  /**
   * The bottom bar, which did not fit and could not be scrolled.
   *
   * `grid-auto-flow: column` sizes each column to its content, so eleven items for a diner
   * and TWELVE for an administrator came to 419px inside a 390px phone — and with the
   * default `overflow-x: visible` the surplus was simply clipped. "Settings" was cut to
   * "Se" and there was no way to reach it. `Add a place` wrapped onto three lines trying
   * to fit, which is why the bar was also far taller than it needed to be.
   *
   * Now it scrolls. `minmax(64px, 1fr)` gives every item a tappable floor and lets them
   * share the width when they do fit, so a deployment with `nav.hidden` set — or a diner
   * in simple mode, who sees four fewer — gets the old evenly-spread bar and no scrolling
   * at all. Only the crowded case scrolls.
   *
   * NOTHING IS HIDDEN. Round 8's rule: anything the rail offers that a person actually
   * needs must stay reachable, and this bar IS the rail on a phone. Dropping items to make
   * them fit would put Settings — the only way back out of simple mode — behind nothing at
   * all. `nav.hidden` remains the operator's control for genuinely not wanting a page.
   */
  .tabbar {
    display: grid; grid-auto-flow: column; grid-auto-columns: minmax(64px, 1fr);
    position: fixed; inset: auto 0 0 0; z-index: 40;
    background: var(--surface); border-top: 1px solid var(--line);
    padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
    overflow-x: auto;
    /* Momentum on iOS, and each item comes to rest under the thumb rather than half off. */
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
  }
  .tabbar a {
    text-decoration: none; text-align: center; padding: 7px 2px; border-radius: var(--radius-sm);
    font: 600 10.5px/1.35 var(--body); color: var(--muted);
    /* One line each. Wrapping is what made "Add a place" three rows tall and pushed the
       bar up over the content it sits under. */
    white-space: nowrap; scroll-snap-align: center;
  }
  .tabbar a.on { color: var(--accent); background: var(--accent-soft); }
  .tabbar a i { display: block; font-style: normal; font-size: 15px; margin-bottom: 2px; }
}

/* ── Primitives ─────────────────────────────────────────────────────────── */
.card {
  background: var(--surface); border: var(--border-w) solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.pad { padding: var(--card-pad); }
.stack { display: flex; flex-direction: column; gap: var(--stack-gap); }
.stack-sm { display: flex; flex-direction: column; gap: 7px; }
.stack-xs { display: flex; flex-direction: column; gap: 4px; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.row-tight { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

/* The strip of links to the same thing on another screen - a listing's public page, its
   venue console and its administrator's editor. Marked out from the buttons around it
   because it navigates rather than changes anything, which is the distinction a person
   about to click needs. */
.cross-links { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--line); }
.cross-links .btn { font-size: 12px; }

/* ── Social links ─────────────────────────────────────────────────────────
   The row of outbound links on a listing page, the compact dots on a card, and the
   editor's one-box-per-network grid. Marks are drawn at their natural size and never
   scaled up: an operator's 48px upload stretched to fill a button looks worse than the
   character it replaced. */
.social-links { display: flex; flex-wrap: wrap; gap: 6px; }
.social-link { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; }
.social-link span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.social-dots { gap: 4px; }
.social-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; border-radius: 7px;
  border: 1px solid var(--line); background: var(--surface-2);
  text-decoration: none; line-height: 1;
}
.social-dot:hover { border-color: var(--accent-line, var(--line)); }
.social-field label { display: flex; align-items: center; gap: 6px; }
.social-mark-preview {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface-2);
}

/* ── Check in from where you are ──────────────────────────────────────────
   The strip on the landing screen. A candidate is a full-width button rather than a row
   with a button in it: on a phone the whole row is the target, and a check-in taken
   standing at a bar should not need aiming. */
.checkin-here { margin-bottom: 16px; }
.checkin-candidate {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 9px 8px; text-align: left;
  background: none; border: 0; border-radius: 8px; cursor: pointer;
  color: inherit; font: inherit;
}
.checkin-candidate:hover { background: var(--surface-2); }
.checkin-candidate.on { background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--accent-line, var(--line)); }

/* ── Dish photographs ─────────────────────────────────────────────────────
   A strip of square thumbnails under a dish, and one full-size image over the page. The
   thumbnails are a fixed grid so a row of them stays a row whatever shape the pictures
   were taken in; the full size keeps its own shape, which is why it is not squared. */
.dish-photos { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 10px; }
.dish-photo {
  position: relative; width: 56px; height: 56px; padding: 0;
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
  background: var(--surface-2); cursor: pointer;
}
.dish-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dish-photo.mine { border-color: var(--accent-line, var(--line)); }
.dish-photo-mark {
  position: absolute; right: 2px; bottom: 1px; font-size: 10px; line-height: 1;
  text-shadow: 0 0 3px rgba(0, 0, 0, .6);
}
.dish-photo.add {
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; color: var(--muted); border-style: dashed;
}
.dish-photo-full {
  display: block; width: 100%; height: auto; max-height: 60vh;
  object-fit: contain; border-radius: 10px; background: var(--surface-2);
}

/* ── The review queue ─────────────────────────────────────────────────────
   Collapsed by default, so an operator who came to fix one restaurant's phone number does
   not scroll past everything waiting for a decision to reach the table. The header is a
   real <button> rather than a div with a click handler: it is a disclosure control, and
   the keyboard and every screen reader should be told so. */
.queue-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; padding: 2px 0; margin: 0;
  background: none; border: 0; cursor: pointer; color: inherit; font: inherit;
  text-align: left;
}
.queue-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.queue-chevron { flex: 0 0 auto; font-size: 13px; color: var(--muted); }
.queue-head:hover .queue-chevron { color: var(--text); }

/* One row in the queue. Two named blocks rather than two anonymous flex children, so a
   phone can stack them — see the note above reviewRow for what the old shape did at
   375px. */
.review-row-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
}
.review-row-id { min-width: 0; flex: 1 1 auto; }
.review-row-name { overflow-wrap: anywhere; }
.review-actions { flex: 0 0 auto; }
.suggestion-row { border-top: 1px solid var(--line); padding-top: 8px; }
.suggestion-row:first-child { border-top: 0; padding-top: 0; }

/* The "⋯" on a listing. Square and quiet: it sits next to the pin, and everything behind
   it is rare enough that it should not compete with the thing the page is for. */
.listing-more { min-width: 34px; padding-left: 8px; padding-right: 8px; letter-spacing: 1px; }

@media (max-width: 640px) {
  /* Stacked, and the buttons share the width evenly instead of each taking as much as its
     label wants. `min-width: 0` is load-bearing: without it a flex item refuses to shrink
     below its content and the row overflows the card rather than wrapping inside it. */
  .review-row-head { flex-direction: column; align-items: stretch; }
  .review-actions { width: 100%; }
  .review-actions .btn { flex: 1 1 auto; min-width: 0; justify-content: center; }
  .suggestion-row .spread { flex-direction: column; align-items: flex-start; }
}
.spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); }
.grid-2 { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.scroll-x { overflow-x: auto; }

/* ── Admin tables on a phone ──────────────────────────────────────────────
   These tables carry up to nine columns and every cell is `white-space: nowrap`, so on a
   375px screen the users table laid out 687px wide inside a 314px box. Everything past
   the second column — including every action button — sat off the right edge, inside a
   nested scroller most people never think to drag. It read as "the buttons do nothing".

   So below 720px each ROW becomes a card and each CELL becomes a labelled line, with the
   label coming from the `data-label` the markup already has to carry. One set of markup,
   no JavaScript, no second render path to keep in step with the first.

   Deliberately NOT `:has()`, which needs Safari 15.4 — the wrapper opts in by carrying
   `table-wrap` rather than by being detected. Round 33 is recent enough to remember.
   ──────────────────────────────────────────────────────────────────────── */
@media (max-width: 720px) {
  /* The wrapper must stop clipping, or the cards inherit the scroller they replaced. */
  .table-wrap { overflow-x: visible; }

  /* Screen readers keep the header; it is only visually replaced by the per-cell labels. */
  .table-cards thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
  .table-cards, .table-cards tbody, .table-cards tr, .table-cards td { display: block; width: auto; }
  .table-cards tr {
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    padding: 9px 11px; margin-bottom: 10px; background: var(--surface);
  }
  /* The stripe is what separated rows in a table; the card border does that job now. */
  .table-cards tbody tr:nth-child(even) { background: var(--surface); }
  .table-cards td {
    border: 0; padding: 4px 0; white-space: normal;
    display: flex; gap: 12px; align-items: center; justify-content: space-between;
  }
  .table-cards td::before {
    content: attr(data-label);
    font: 600 10px/1.5 var(--body); letter-spacing: .11em; text-transform: uppercase;
    color: var(--muted); flex: 0 0 auto;
  }
  /* A cell with no label is a full-width one — the actions, and anything left blank. */
  .table-cards td:not([data-label]) { display: block; }
  .table-cards td.n { text-align: left; }
  .table-cards td select { max-width: 60%; }
  /* Actions stop being a huddle in the corner and become a row you can hit with a thumb. */
  .table-cards td.actions {
    display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-start;
    margin-top: 7px; padding-top: 9px; border-top: 1px solid var(--line);
  }
  .table-cards td.actions::before { content: none; }
  .table-cards td.actions .btn { flex: 1 1 auto; justify-content: center; }
}

.btn {
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink);
  padding: 7px 13px; border-radius: var(--radius-sm); cursor: pointer;
  font-size: 13.5px; font-weight: 500; transition: background .12s, border-color .12s;
}
.btn:hover { background: var(--surface-2); border-color: var(--muted); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn.primary:hover { filter: brightness(1.08); }
.btn.ghost { border-color: transparent; background: transparent; color: var(--muted); }
.btn.ghost:hover { color: var(--ink); background: var(--surface-2); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.sm { padding: 4px 9px; font-size: 12.5px; }
/* Destructive, and coloured only on hover — a row of red buttons reads as an error
   state rather than as a set of things you may do. */
.btn.danger { color: var(--bad); }
.btn.danger:hover { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); }

.field { display: flex; flex-direction: column; gap: 5px; }
.field > label { font: 600 10px/1 var(--body); letter-spacing: .13em; text-transform: uppercase; color: var(--muted); }
input[type="text"], input[type="email"], input[type="search"], input[type="datetime-local"],
input[type="date"], select, textarea {
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm); padding: 7px 10px; width: 100%;
}
textarea { resize: vertical; min-height: 62px; }
/* Matches .btn.sm, so an input can sit on the same row as a small button without
   dragging the row's height around. */
.input.sm { padding: 4px 9px; font-size: 12.5px; }

.chip {
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-2);
  border-radius: 999px; padding: 2.5px 9px; font-size: 11.5px; white-space: nowrap;
}
.chip.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); font-weight: 600; }
.chip.good { background: var(--good-soft); border-color: transparent; color: var(--good); }
.chip.bad { background: var(--bad-soft); border-color: transparent; color: var(--bad); }
.chip.warn { background: var(--warn-soft); border-color: transparent; color: var(--warn); }

/* Group 14's card tags. `status-visited` carries no tone of its own on purpose: having been
   somewhere is a neutral fact, and colouring it would put three loud chips on every card.
   The other two inherit .good and .bad, so a theme that restyles those restyles these. */
.chip.status-visited { font-weight: 600; opacity: .85; }
.chip.status-new, .chip.status-disliked { font-weight: 600; }
/* Somebody who said they cannot make it. Struck through rather than hidden — the group
   still wants to see that they were asked and answered. */
.chip.out { opacity: .55; text-decoration: line-through; }
/* A claim Palate is hedging. Drawn quieter than the same chip stated firmly, so the
   difference between "we know this" and "a map says this" is visible before the words are
   read. Composes with .good and .bad rather than replacing them: the place is still open
   or shut, it is only the confidence that differs. */
.chip.ghost { background: transparent; border-style: dashed; font-weight: 400; }

/* ── Tag presets ──────────────────────────────────────────────────────────
   A picker can carry sixty chips, so they are smaller than an ordinary chip and the
   block is capped and scrolls rather than pushing the box it belongs to off screen.
   The left rule ties the chips visually to the input above them: without it, three
   preset lists under one text box read as three unrelated sections.
   ───────────────────────────────────────────────────────────────────────── */
.preset-picker {
  border-left: 2px solid var(--line);
  padding: 2px 0 2px 10px;
  margin-top: -2px;
  max-height: 260px;
  overflow-y: auto;
}
.preset-picker .chip { font-size: 11px; padding: 2px 8px; }

/* Tappable, and obviously so — these are buttons, unlike most chips on the site. */
.preset-picker .chip { cursor: pointer; }
.preset-picker .chip:hover { border-color: var(--accent-line); color: var(--accent); }
.preset-picker .chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
button.chip { cursor: pointer; }

.price { font-family: var(--mono); font-size: 12px; color: var(--muted); letter-spacing: .5px; }

/* ── Match meter — the recurring device ─────────────────────────────────── */
.meter { display: flex; align-items: center; gap: 10px; }
.meter-track {
  flex: 1; height: 6px; border-radius: 3px; background: var(--surface-2);
  border: 1px solid var(--line); overflow: hidden; position: relative;
}
.meter-fill { height: 100%; background: var(--accent); border-radius: 3px; transition: width .5s cubic-bezier(.22,.9,.3,1); }
.meter-val { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 17px; font-weight: 600; min-width: 46px; text-align: right; }
.meter.weak .meter-fill { background: var(--muted); }
.meter.strong .meter-val { color: var(--accent); }

/* Provenance trail — how a recommendation reached you */
.path { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); }
.path .node { color: var(--ink-2); }
.path .node.src { color: var(--accent); font-weight: 600; }
.path .arrow { color: var(--line-strong); }
.hopmark {
  font-family: var(--mono); font-size: 10px; padding: 1px 5px; border-radius: 3px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
}

/* ── Score picker: -2 … +2 ──────────────────────────────────────────────── */
.scores { display: inline-flex; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); overflow: hidden; }
.scores button {
  border: 0; background: var(--surface); padding: 5px 10px; cursor: pointer;
  font-family: var(--mono); font-size: 12.5px; color: var(--muted);
  border-right: 1px solid var(--line); transition: background .1s, color .1s;
}
.scores button:last-child { border-right: 0; }
.scores button:hover { background: var(--surface-2); color: var(--ink); }
.scores button.on { font-weight: 700; }
.scores button.on[data-v="2"], .scores button.on[data-v="1"] { background: var(--good); color: #fff; }
.scores button.on[data-v="0"] { background: var(--line-strong); color: var(--ink); }
.scores button.on[data-v="-1"], .scores button.on[data-v="-2"] { background: var(--bad); color: #fff; }
.scores.sm button { padding: 3px 7px; font-size: 11.5px; }

/* ── Dish list ──────────────────────────────────────────────────────────── */
.dish { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-top: 1px solid var(--line); }
.dish:first-child { border-top: 0; }
.dish-name { font-weight: 500; }
.dish-meta { display: flex; gap: 8px; align-items: center; }

/* ── Affinity bars ──────────────────────────────────────────────────────── */
.bars { display: flex; flex-direction: column; gap: 5px; }
.bar-row { display: grid; grid-template-columns: 96px 1fr 42px; align-items: center; gap: 9px; font-size: 12.5px; }
.bar-track { position: relative; height: 14px; background: var(--surface-2); border-radius: 3px; border: 1px solid var(--line); }
.bar-track::before { content: ""; position: absolute; left: 50%; top: 0; bottom: 0; width: 1px; background: var(--line-strong); }
.bar-fill { position: absolute; top: 1px; bottom: 1px; border-radius: 2px; }
.bar-fill.pos { background: var(--good); left: 50%; }
.bar-fill.neg { background: var(--bad); right: 50%; }

/* ── Tables ─────────────────────────────────────────────────────────────── */
table { border-collapse: collapse; width: 100%; font-size: 13px; }
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); white-space: nowrap; }
th { font: 600 10px/1 var(--body); letter-spacing: .11em; text-transform: uppercase; color: var(--muted); }
td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* Alternating rows: stripes carry the palette tint, hover stays distinct on top. */
tbody tr:nth-child(even) { background: var(--stripe); }
tbody tr:hover { background: var(--surface-2); }
/* A select in a table cell is as wide as its longest option (group 35). The global
   `select { width: 100% }` means 100% of a cell an auto-layout table sizes LAST, so on the
   live restaurant list the review select was squeezed to 42px and drew as a bare arrow —
   "the review dropdown is blank". The card layout below 720px still caps it at 60%. */
td select { width: auto; }
/* In the console's tables a name, an address and a town may wrap, so the width goes to the
   controls rather than to one long name or email. */
.table-wrap td[data-label="Name"], .table-wrap td[data-label="Email"] {
  white-space: normal; overflow-wrap: anywhere; min-width: 9rem;
}
.table-wrap td[data-label="City"] { white-space: normal; }
.restaurant-table th.n { white-space: normal; max-width: 7rem; line-height: 1.35; }
/* The actions column is pinned to the scroller's right edge, so Edit and Delete are on
   screen whatever the table does: a theme at 150% type, a long unbroken name. The cell
   needs an opaque background or the columns scrolling under it show through, and the
   stripe is a tint, so it is laid over the surface rather than used alone. */
.table-wrap td.actions, .table-wrap th.actions {
  position: sticky; right: 0; background: var(--surface);
  box-shadow: -8px 0 8px -8px rgba(0, 0, 0, .18);
}
.table-wrap tbody tr:nth-child(even) td.actions {
  background: linear-gradient(var(--stripe), var(--stripe)), var(--surface);
}
.table-wrap tbody tr:hover td.actions { background: var(--surface-2); }
@media (max-width: 720px) {
  /* A card is not a scroller: nothing to pin to, and no edge to shade. After the rules
     above on purpose, and as specific as the most specific of them, so it wins. */
  .table-wrap td.actions, .table-wrap th.actions,
  .table-wrap tbody tr:nth-child(even) td.actions,
  .table-wrap tbody tr:hover td.actions { position: static; box-shadow: none; background: none; }
}
.dish:nth-child(even) { background: var(--stripe); }
.dish { padding-left: 8px; padding-right: 8px; margin: 0 -8px; border-radius: 4px; }
.list-striped > *:nth-child(even) { background: var(--stripe); }
/* Opening hours: seven rows in a sidebar card, so the day column is sized to the longest
   weekday and the times take the rest. `white-space: normal` on the cell because a day
   with a lunch and a dinner service carries two ranges and must wrap rather than push the
   card wide. */
.hours-table { font-size: 12.5px; }
.hours-table th { width: 84px; font-size: 10px; }
.hours-table td { white-space: normal; }
/* The venue's own seven-row editor. Tighter padding than a data table because it is seven
   rows of three controls inside a form, and a disabled time box is dimmed rather than
   hidden so the row still reads as a day that exists. */
.hours-edit td { padding: 3px 6px; }
.hours-edit th { width: 92px; }
.hours-edit input[type="time"] { padding: 3px 6px; font-size: 12.5px; }
.hours-edit input[type="time"]:disabled { opacity: .4; }
.list-bordered > * { border: 1px solid var(--line); border-radius: var(--radius-sm); }
.cell-score { display: inline-block; min-width: 30px; text-align: center; border-radius: 3px; padding: 1px 4px; font-family: var(--mono); font-size: 11.5px; }
.cell-score.p2 { background: var(--good); color: #fff; }
.cell-score.p1 { background: var(--good-soft); color: var(--good); }
.cell-score.z { background: var(--surface-2); color: var(--muted); }
.cell-score.n1 { background: var(--bad-soft); color: var(--bad); }
.cell-score.n2 { background: var(--bad); color: #fff; }

/* ── Voting ─────────────────────────────────────────────────────────────── */
.ballot-item {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); cursor: pointer;
}
.ballot-item:hover { border-color: var(--line-strong); }
.ballot-item.picked { border-color: var(--accent); background: var(--accent-soft); }
.rank-badge {
  width: 24px; height: 24px; flex: none; border-radius: 50%; display: grid; place-items: center;
  font-family: var(--mono); font-size: 11px; border: 1px solid var(--line-strong); color: var(--muted); background: var(--surface);
}
.ballot-item.picked .rank-badge { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 700; }

.round { border-left: 2px solid var(--line); padding: 0 0 14px 14px; position: relative; }
.round:last-child { padding-bottom: 0; }
.round::before {
  content: ""; position: absolute; left: -5px; top: 4px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--line-strong);
}
.round.final::before { background: var(--accent); }
.tally-row { display: grid; grid-template-columns: 1fr 46px 90px; align-items: center; gap: 8px; font-size: 12.5px; padding: 2px 0; }
.tally-bar { height: 8px; background: var(--surface-2); border-radius: 2px; border: 1px solid var(--line); overflow: hidden; }
.tally-bar span { display: block; height: 100%; background: var(--accent); }
.tally-row.out { opacity: .45; text-decoration: line-through; }
.tally-row.out .tally-bar span { background: var(--muted); }

/* ── Misc ───────────────────────────────────────────────────────────────── */
.stat { display: flex; flex-direction: column; gap: 1px; }
.stat b { font-family: var(--mono); font-size: 22px; font-weight: 600; letter-spacing: -.02em; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 14px; }

.empty { text-align: center; padding: 40px 20px; color: var(--muted); }
.divider { height: 1px; background: var(--line); }
.linkish { color: var(--accent); text-decoration: none; cursor: pointer; font-weight: 500; }
.linkish:hover { text-decoration: underline; }
.clickable { cursor: pointer; }
.card.clickable:hover { border-color: var(--line-strong); }

/* ── Auth ───────────────────────────────────────────────────────────────── */
.auth-page {
  min-height: 100vh; display: grid; grid-template-columns: 1.1fr .9fr;
  gap: 48px; align-items: center; padding: 40px 6vw; max-width: 1180px; margin: 0 auto;
}
.auth-pitch h1 {
  font-size: clamp(1.9rem, 3.4vw, 2.7rem); line-height: 1.1; margin: 22px 0 14px; max-width: 16ch;
}
.auth-pitch p { color: var(--ink-2); max-width: 46ch; }
.auth-points { list-style: none; margin: 22px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; max-width: 48ch; }
.auth-points li {
  font-size: 13.5px; color: var(--muted); padding-left: 15px; position: relative; line-height: 1.5;
}
.auth-points li::before {
  content: ""; position: absolute; left: 0; top: .62em; width: 6px; height: 6px;
  border-radius: 50%; background: var(--accent);
}
.auth-points b { color: var(--ink); font-weight: 600; }

.auth-card { max-width: 400px; width: 100%; }
.auth-tabs { display: flex; gap: 2px; margin-bottom: 18px; border-bottom: 1px solid var(--line); }
.auth-tab {
  border: 0; background: none; padding: 8px 2px; margin-right: 18px; cursor: pointer;
  font-size: 14px; font-weight: 500; color: var(--muted); border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.auth-tab.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.auth-tab:hover { color: var(--ink); }
.auth-error {
  background: var(--bad-soft); color: var(--bad); border-radius: var(--radius-sm);
  padding: 9px 12px; font-size: 13px; margin-bottom: 14px;
}
/* Good news on the sign-in page — a confirmed address, not a failure. */
.auth-notice {
  background: var(--good-soft); color: var(--good); border-radius: var(--radius-sm);
  padding: 9px 12px; font-size: 13px; margin-bottom: 14px;
}

.profile-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); cursor: pointer; text-align: left; font-size: 13.5px;
}
.profile-row:hover { border-color: var(--accent-line); background: var(--accent-soft); }

@media (max-width: 900px) {
  .auth-page { grid-template-columns: 1fr; gap: 28px; padding: 32px 20px; align-content: start; }
  .auth-card { max-width: none; }
  .auth-points { display: none; }
}

/* ── Onboarding wizard ──────────────────────────────────────────────────── */
.wizard { max-width: 660px; margin: 0 auto; padding: 30px 0 60px; }
.wizard .lede { color: var(--muted); font-size: 14px; margin-top: 6px; max-width: 58ch; }
.wizard-progress { position: sticky; top: 10px; z-index: 5; }
.wizard-bars { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.wizard-bar { display: flex; flex-direction: column; gap: 4px; }

.wizard-card h2 { font-size: 1.6rem; }
.wizard-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.wizard-opinions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.wizard-choice {
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink);
  border-radius: var(--radius-sm); padding: 11px 10px; cursor: pointer;
  font-size: 13.5px; font-weight: 500; transition: background .12s, border-color .12s, color .12s;
}
.wizard-choice:hover { border-color: currentColor; }
.wizard-choice.love:hover, .wizard-choice.yes:hover { color: var(--good); background: var(--good-soft); }
.wizard-choice.no:hover, .wizard-choice.avoid:hover { color: var(--bad); background: var(--bad-soft); }
.wizard-choice.love { font-weight: 600; }
.wizard-skip { width: 100%; color: var(--muted); }

@media (min-width: 620px) {
  .wizard-actions { flex-direction: row-reverse; align-items: stretch; }
  .wizard-opinions { flex: 1; grid-template-columns: repeat(4, 1fr); }
  .wizard-skip { width: auto; flex: 0 0 116px; }
}

.rail-nudge {
  display: flex; flex-direction: column; gap: 2px; text-decoration: none;
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm); padding: 9px 11px;
}
.rail-nudge b { color: var(--accent); font-size: 12.5px; }
.rail-nudge span { color: var(--ink-2); font-size: 11.5px; line-height: 1.4; }
.rail-account { display: flex; flex-direction: column; gap: 1px; padding: 0 2px; }

/* ── Offers & coupons ───────────────────────────────────────────────────── */
.offer-card.locked { opacity: .72; }
.offer-card.eligible { border-color: var(--accent-line); }
.offer-value {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  background: var(--surface-2); border-radius: var(--radius-sm); padding: 9px 11px;
}
.offer-value b { font-family: var(--display); font-size: 1.05rem; }
.offer-save {
  font: 600 11px/1 var(--body); letter-spacing: .04em; text-transform: uppercase;
  color: var(--good); white-space: nowrap;
}
.offer-checks { display: flex; flex-direction: column; gap: 4px; }
.check { display: flex; gap: 7px; align-items: flex-start; font-size: 12.5px; }
.check span:first-child { font-family: var(--mono); width: 12px; flex: none; }
.check.ok { color: var(--ink-2); }
.check.ok span:first-child { color: var(--good); }
.check.no { color: var(--muted); }
.check.no span:first-child { color: var(--bad); }

.coupon.spent { opacity: .6; }
.coupon-qr {
  display: grid; place-items: center; padding: 12px; background: #fff;
  border-radius: var(--radius-sm); border: 1px solid var(--line);
}
.coupon-qr svg { width: 168px; height: 168px; display: block; }
.coupon-code {
  text-align: center; font-size: 20px; letter-spacing: .16em; font-weight: 600;
  padding: 7px; background: var(--surface-2); border-radius: var(--radius-sm);
  border: 1px dashed var(--line-strong);
}

/* ── Tabs, settings rows, choices ───────────────────────────────────────── */
.tabs { display: flex; gap: 20px; border-bottom: 1px solid var(--line); margin-bottom: 18px; overflow-x: auto; }

/* ── A tab strip on a phone ──────────────────────────────────────────────────
   The operator console has eighteen tabs and the venue console eleven, which on a 375px
   screen is about three times the width of the display. `overflow-x: auto` above meant it
   always scrolled — but iOS draws no scrollbar until you are already dragging, so nothing
   said the rest was there, and swiping through three screens to reach "Audit" is a poor
   way to reach it even once you know.

   So below 700px the same tabs are a <select> instead: one control, everything one tap
   away, and no horizontal scrolling at all. Both are in the DOM and CSS picks — choosing
   in JavaScript from innerWidth would be right until somebody rotated the phone, because
   these screens re-render on navigation rather than on resize.
   ────────────────────────────────────────────────────────────────────────── */
.tabs-narrow { display: none; margin-bottom: 18px; }
.tabs-narrow select { width: 100%; }

@media (max-width: 700px) {
  .tabs-narrow { display: block; }
  .tabs-wide { display: none; }
}
.tab {
  border: 0; background: none; padding: 9px 0; cursor: pointer; white-space: nowrap;
  font-size: 14px; font-weight: 500; color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab.on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab:hover { color: var(--ink); }
.tab-count {
  display: inline-block; min-width: 17px; padding: 1px 5px; margin-left: 5px;
  border-radius: 9px; background: var(--surface-2); color: var(--muted);
  font-size: 11px; font-weight: 600; text-align: center; vertical-align: 1px;
}
.tab.on .tab-count { background: var(--accent-soft); color: var(--accent); }

/* ── Location, browse, pins ─────────────────────────────────────────────── */
.gps-btn { display: inline-flex; align-items: center; gap: 6px; }
.gps-icon { font-size: 13px; line-height: 1; }
/* A target that spins while it is looking, so a slow fix does not read as a dead button. */
.gps-btn.busy .gps-icon { animation: gps-seek 1.1s linear infinite; }
@keyframes gps-seek { to { transform: rotate(360deg); } }
.gps-results { margin-top: 8px; }
.gps-error { color: var(--warn); margin-top: 6px; }

/* A session that got in on a PIN (2026-09-17). Shaped like the view-as banner because it is
   the same kind of statement — "you are not in the ordinary state" — and coloured like a
   warning rather than an error: nothing is wrong, something is restricted. */
.pin-session-banner { background: var(--warn-soft); color: var(--text); }

/* ── A food truck's pitch (group 34) ─────────────────────────────────────────
   The panel sits above the console's tabs, so it is the first thing on every screen — and
   the button inside it is the only control on the console that somebody presses while
   standing in a car park, which is why it is full width on a phone rather than a chip in a
   row. `.truck-line` is the same pitch on a card and on the listing, one line, never wrapped
   into a second paragraph.
   ────────────────────────────────────────────────────────────────────────── */
.truck-panel { border-left: 3px solid var(--accent); }
.truck-pin { display: flex; flex-direction: column; gap: 2px; }
.truck-where { font-weight: 700; font-size: 17px; }
.truck-note-line { font-size: 13px; color: var(--text); }
.truck-confirm {
  padding: 10px; border-radius: var(--radius-sm);
  background: var(--accent-soft); border: 1px solid transparent;
}
.truck-line { color: var(--accent); font-weight: 600; }
@media (max-width: 560px) {
  .truck-panel .btn.primary { width: 100%; justify-content: center; padding: 12px 14px; }
}

.browse-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 8px; border-radius: var(--radius-sm);
}
.browse-row .clickable { cursor: pointer; }
.browse-row:hover { background: var(--surface-2); }

.pin { display: inline-flex; align-items: center; gap: 5px; }
.pin.on { color: var(--accent); border-color: var(--accent-line); }
.pin-mark { color: var(--accent); font-size: 15px; line-height: 1; }

/* ── Review queue ───────────────────────────────────────────────────────── */
.dup-block {
  border-top: 1px solid var(--line); padding-top: 8px;
  border-left: 3px solid var(--warn); padding-left: 10px; margin-left: -2px;
}
.note-row { border-top: 1px solid var(--line); padding-top: 6px; }
.note-input { flex: 1; min-width: 0; }

.similar-note {
  display: flex; gap: 6px; align-items: baseline;
  color: var(--muted); border-top: 1px solid var(--line); padding-top: 8px;
}
.similar-note b { color: var(--ink-2); font-weight: 600; }

.setting-row {
  display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: center;
  padding: 11px 0; border-top: 1px solid var(--line);
}
.setting-row:first-of-type { border-top: 0; }
.setting-control { min-width: 150px; display: flex; justify-content: flex-end; }
@media (max-width: 600px) {
  .setting-row { grid-template-columns: 1fr; gap: 8px; }
  .setting-control { justify-content: flex-start; }
}

.toggle-row { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
.toggle-row input { width: 16px; height: 16px; accent-color: var(--accent); }

.choice-row {
  display: flex; align-items: flex-start; gap: 11px; width: 100%; text-align: left;
  padding: 12px 13px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); cursor: pointer;
}
.choice-row:hover:not(:disabled) { border-color: var(--line-strong); }
.choice-row.on { border-color: var(--accent); background: var(--accent-soft); }
.choice-row:disabled { opacity: .5; cursor: not-allowed; }
.choice-dot {
  width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--line-strong);
  flex: none; margin-top: 2px;
}
.choice-row.on .choice-dot { border-color: var(--accent); background: var(--accent); box-shadow: inset 0 0 0 3px var(--surface); }

/* ── Star pickers & facet scorecards ────────────────────────────────────── */
.stars { display: inline-flex; gap: 1px; }
.star {
  border: 0; background: none; cursor: pointer; padding: 1px 2px;
  font-size: 21px; line-height: 1; color: var(--line-strong);
  transition: color .1s, transform .08s;
}
.star:hover { transform: scale(1.15); }
.star.on { color: var(--warn); }
.stars-static { color: var(--warn); font-size: 14px; letter-spacing: 1px; }
.facet-row { display: grid; grid-template-columns: 76px 1fr; align-items: center; gap: 10px; }
.facet-row > label { font-size: 13px; color: var(--ink-2); }
.facet-chip {
  display: inline-flex; gap: 6px; align-items: baseline;
  background: var(--stripe); border-radius: 999px; padding: 3px 10px; font-size: 11.5px;
}
.facet-chip b { color: var(--ink); }
.facet-chip .stars-static { font-size: 11px; }

/* ── Ad slot ─────────────────────────────────────────────────────────────── */
.ad-slot {
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  padding: 14px; text-align: center; margin: 14px 0;
  background: var(--stripe);
}
.ad-slot .ad-label {
  font: 600 9px/1 var(--body); letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); display: block; margin-bottom: 6px;
}
.ad-slot ins { display: block; min-height: 90px; }

/* ── Venue picker ───────────────────────────────────────────────────────── */
.venue-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
.venue-card {
  display: flex; gap: 10px; align-items: center; text-align: left; cursor: pointer;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface);
}
.venue-card:hover { border-color: var(--line-strong); }
.venue-card.on { border-color: var(--accent); background: var(--accent-soft); }
.venue-icon { font-size: 26px; line-height: 1; flex: none; }
.venue-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.venue-meta b { font-size: 13.5px; }

/* Restaurant icon shown alongside names in lists and cards. */
.r-icon { font-size: 20px; line-height: 1; margin-right: 6px; }

/* An uploaded icon, drawn at whatever box the caller asked for. `object-fit: contain`
   rather than cover: an operator's square PNG and somebody's 3:2 photo both have to sit in
   the same row without one of them being cropped through the middle. */
.icon-img { object-fit: contain; vertical-align: -0.15em; display: inline-block; }
.icon-glyph { display: inline-block; vertical-align: -0.05em; }

/* The icon choosers. Scrolls inside itself rather than pushing the form around — a library
   of forty icons under one label would otherwise move the Save button off the screen. */

/* ── The checked-in bar ──────────────────────────────────────────────────────
   Group 18. Fixed to the bottom, because it is a thumb-reach control on a phone somebody is
   holding at a table, and the top of the page already carries the heading.

   THE BODY GETS PADDING FOR IT, which is the part that is easy to forget: a fixed element
   covers whatever the page ends with, so the last row of every list would sit underneath it
   for anybody checked in. `--here-bar-h` is that padding, and it is applied to the scroll
   container rather than to the bar so the two cannot disagree.

   `env(safe-area-inset-bottom)` keeps it clear of the home indicator on a phone without a
   button; it resolves to 0 everywhere else, so it costs nothing to include.
   ────────────────────────────────────────────────────────────────────────── */
/* A STARTING VALUE, replaced by a measurement — see measureHereBar in ui.js. Hard-coding
   it was wrong the moment round 57 let an operator set the base text size: a theme at 22px
   makes this bar visibly taller, and a fixed number would leave the last row of every list
   underneath it. The number here only has to be sensible for the first paint. */
:root { --here-bar-h: 58px; }

.here-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px));
  background: var(--surface);
  border-top: 1px solid var(--accent-line);
  box-shadow: 0 -2px 14px -6px rgba(0, 0, 0, .25);
}

/* The whole left-hand side is the link back. A bar somebody has to aim at a small word in
   is a bar they will not use with one hand. */
.here-go {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 9px;
  background: none; border: 0; padding: 4px 2px; margin: 0;
  color: var(--ink); font: inherit; text-align: left; cursor: pointer;
}
.here-go:hover b { text-decoration: underline; }
.here-go:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }

/* Not an animation. A dot that pulsed would be a notification, and this is a statement of
   fact that is true for hours — something blinking at somebody all through dinner is the
   sort of thing people turn an app off over. */
.here-dot {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%;
  background: var(--good); box-shadow: 0 0 0 3px var(--good-soft);
}

.here-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
/* RELATIVE TO THE THEME, not a fixed pixel size. body is set to var(--base-size), so an
   operator who raised the site text for legibility raises this too — a bar that stayed at
   14px would be the one thing on the page they still could not read. The measurement in
   ui.js is what keeps the reserved space honest when that happens. */
.here-text b { font-size: .95em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* One line, always. It is the secondary half of a two-line label in a fixed bar, and
   letting it wrap to three lines makes the bar tall enough to eat the page. */
.here-text .tiny {
  color: var(--muted); font-size: .76em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.here-out { flex: 0 0 auto; }

/* What the bar would otherwise cover. ADDED to the page's own bottom padding rather than
   replacing it — replacing it came to less than the padding already there, which would have
   removed room on the one screen that needed more. Only while the bar exists: `:has()`
   keeps it off every page nobody is checked in on. */
body:has(.here-bar) .main {
  padding-bottom: calc(var(--main-pad-bottom) + var(--here-bar-h));
}

/* `:has()` is not everywhere yet, and on Safari 13 — which this codebase still supports —
   the whole selector is dropped. Reserving the room unconditionally there is the safe way
   to be wrong: a little empty space at the bottom of a page, rather than a bar sitting on
   top of the last row of a list. */
@supports not selector(:has(*)) {
  .main { padding-bottom: calc(var(--main-pad-bottom) + var(--here-bar-h)); }
}

/* ── A badge is a square mark, and its name is a tooltip ────────────────────
   A badge used to be a chip with its name written in it, which meant three badges ate the
   width of a card and the cuisine tags went under. It is now ONE SQUARE holding only the
   icon, and the name appears on hover or keyboard focus.

   SQUARE, AND SQUARE AT EVERY SIZE. `.chip` is a 999px pill, so this has to say otherwise;
   the corner is 0.22em rather than `--radius-sm` because a theme's small radius runs from 0
   to 12px and 12px on a 24px box is a circle, not a badge. In em it stays the same shape
   whether the badge is drawn at 20px or at 46px.

   THE NAME IS NEVER ONLY IN THE TOOLTIP. `aria-label` carries it for assistive technology,
   `title` carries it for a long-press and for browsers that surface it, and `.sr-only`
   keeps it in the text of the page. A name reachable only by hovering is a name that does
   not exist on a phone, and the diners this is for are holding phones at a table.
   ────────────────────────────────────────────────────────────────────────── */
/* Saved searches (group 24.5). A row is a name you press and a Remove beside it; the panel
   sits under the box because that is where the button that opens it is. */
.saved-panel { margin-top: 8px; }
.saved-row { display: flex; align-items: center; gap: 6px; }
.saved-row .btn { margin: 0; }
.saved-row b { font-weight: 600; }

/* The "when" row under the search box (round 67). One line of buttons that wraps, with the
   date box at the end — the presets answer nine questions out of ten and the box is there
   for the tenth, which is the order somebody reaches for them in. */
.when-row {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  padding-top: 2px;
}
.when-row .when-at {
  margin: 0; flex: 0 1 auto; min-width: 0; max-width: 100%;
  font-size: 12.5px; padding: 5px 7px;
}
@media (max-width: 560px) {
  .when-row .when-at { flex: 1 1 100%; }
}

/* The once-only PIN panel (group 26). Loud on purpose: it is the only time this number is
   ever on a screen, and a person who scrolls past it has lost it for good. */
.new-pin {
  background: var(--warn-soft); color: var(--warn); border-color: transparent;
  max-width: 560px; margin-bottom: 14px;
}
.new-pin h2 { font-size: 2.2rem; margin: 0; }
.new-pin .small, .new-pin .tiny { color: inherit; opacity: .9; }

/* A card carrying a refusal, in the place the refused thing was (group 25). The colours are
   the banner's, so a person who has already seen the band at the top recognises this as the
   same subject rather than a second unrelated problem. */
.warn-card { background: var(--warn-soft); color: var(--warn); border-color: transparent; }
.warn-card .tiny { color: inherit; opacity: .85; }

/* The unverified band (group 25). Above everything, like the view-as band, because it
   explains the state the whole screen is in rather than one panel - every button on the page
   is about to refuse, and a person deserves to know that before they press one. */
.unverified-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; padding: 9px 14px;
  background: var(--warn-soft); color: var(--warn);
  border-bottom: 1px solid var(--warn);
}
.unverified-banner .tiny { color: inherit; opacity: .85; }
@media (max-width: 560px) { .unverified-banner { flex-direction: column; align-items: stretch; } }

/* Temporary access (roles phase 3). Borrowed power is said on every screen, like view-as,
   in the accent rather than the warning colour: nothing is wrong, it just ends. */
.temp-access-banner {
  display: flex; align-items: center; gap: 8px 14px; flex-wrap: wrap; padding: 9px 14px;
  background: var(--accent-soft); border-bottom: 1px solid var(--accent);
}
.temp-access-banner .grant-line { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.grant-row {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 8px 12px;
  flex-wrap: wrap; padding: 10px 0; border-top: 1px solid var(--line);
}
.eyebrow + .grant-row, .tiny + .grant-row { border-top: 0; }
.grant-pin { font: 600 32px/1.1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; letter-spacing: .12em; }

/* The view-as banner (group 23, moved to the shell in round 66).
   ACROSS THE TOP OF EVERY SCREEN and deliberately loud: the failure mode of this feature is
   somebody forgetting they are wearing it and reporting a bug about a missing button. It
   sits above the operator's own banner, because "you are not yourself right now" outranks
   whatever the site has to say. */
.view-as-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; padding: 9px 14px;
  background: var(--warn-soft); color: var(--warn);
  border-bottom: 1px solid var(--warn);
  position: sticky; top: 0; z-index: 40;
}
.view-as-banner b { font-size: .95rem; }
.view-as-banner .tiny { color: inherit; opacity: .85; }
@media (max-width: 560px) {
  .view-as-banner { flex-direction: column; align-items: stretch; }
}

/* The owner's what's-on editor (group 22). A row is a kind, a time, and a note, with the
   remove button pinned to the end — and on a phone it stacks, because three time inputs and
   a note will not sit on one line at 375px and squeezing them makes every one unusable.

   `.past` is a one-off that has been and gone. It stays in the EDITOR — nothing deletes a
   venue's own rows behind their back — and is dimmed rather than hidden, so a venue can see
   why it is no longer on their listing and remove it themselves. */
.wo-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.wo-row.past { opacity: .6; }
/* A schedule row (group 38): its controls wrap in lines, and a Closed row says so in colour
   before it is read, because it is the row that beats every other. */
.wo-row.wo-slot { align-items: flex-start; padding: 8px 0; border-top: 1px solid var(--line); }
.wo-row.wo-slot.closed { box-shadow: inset 3px 0 0 var(--bad); padding-left: 8px; }
.wo-row.locked { opacity: .8; }
.wo-days { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.wo-day {
  font: inherit; font-size: 12px; min-width: 2.6em; padding: 3px 6px; border-radius: 999px;
  border: 1px solid var(--line); background: transparent; color: var(--ink-2, inherit); cursor: pointer;
}
.wo-day.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }
.wo-more summary { cursor: pointer; }
.hours-exceptions { margin: 4px 0 0; padding-left: 18px; }
/* What a card says about right now (group 39). Three tones, and they are the three a reader
   needs to tell apart at a glance: a warning, a refusal, and news. */
.status-warn { color: var(--warn); }
.status-bad { color: var(--bad); }
.status-good { color: var(--good); }
.chip.whats-on.status-warn { color: var(--warn); border-color: var(--warn); }
.chip.whats-on.status-bad { color: var(--bad); border-color: var(--bad); }
.chip.whats-on.status-good { color: var(--good); border-color: var(--good); }
.wo-row input, .wo-row select { margin: 0; }
@media (max-width: 620px) {
  .wo-row { flex-direction: column; align-items: stretch; }
  .wo-row > .row-tight:last-child { justify-content: flex-end; }
}

/* What is on, on a card. The accent edge is the same "this is the row you were looking for"
   mark `card-lit` uses, and it is deliberately NOT a badge: badges here carry a promise that
   they were not written by the venue, and a happy hour is. See shared/whatson.js. */
.chip.whats-on { max-width: 100%; white-space: normal; text-align: left; }

/* THE ONE THEY ASKED FOR IS LOUD — group 22, sharpened in round 67.
   A card in a list of twenty is scanned, not read, so the thing somebody typed has to be
   the thing their eye lands on. `.loud` is filled rather than outlined and carries the
   accent's ink; `.quiet` is the same shape for a place that HAS the thing but has not said
   when, so the two read as one family with different volumes.

   Everything else on the card keeps its ordinary weight. A card that shouted every fact
   equally would be a card that shouted nothing. */
.chip.whats-on.loud {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
  font-weight: 600;
}
.chip.whats-on.loud.quiet {
  background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent);
}

/* CLOSING SOON is a warning, not an offer, so it never borrows the accent — somebody
   scanning for happy hour must not read "shuts in ten minutes" as the good news. */
.chip.whats-on.closing {
  background: var(--warn-soft); border-color: var(--warn-soft); color: var(--warn);
  font-weight: 600;
}
.chip.whats-on.closing.urgent { background: var(--warn); color: var(--bg); border-color: var(--warn); }

/* The venue's floor list (group 21). A row is a count and, if the table chose one, a name —
   never an identity; shared/floor.js carries the argument. `.needs` is the one row a server
   has to walk over to, so it gets the accent edge and nothing else does: a list where every
   row is highlighted is a list with no highlight in it. */
.floor-list { display: flex; flex-direction: column; gap: 6px; }
.floor-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.floor-row.needs { border-color: var(--accent-line); background: var(--accent-soft); }
.floor-row > .row-tight { flex: 0 1 auto; }
@media (max-width: 520px) {
  .floor-row { flex-direction: column; align-items: stretch; }
  .floor-row > .row-tight { justify-content: flex-start; }
}

.badge-chip {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.15em; height: 2.15em;
  border-radius: 0.22em;
  font-weight: 600;
}

/* The name itself. Hidden rather than absent so it is in the accessibility tree, and
   `visibility` rather than `display` so it can be transitioned. */
.badge-name {
  position: absolute; bottom: calc(100% + 5px); left: 50%;
  transform: translateX(-50%) translateY(2px);
  z-index: 40; pointer-events: none;
  background: var(--ink); color: var(--surface);
  border-radius: var(--radius-sm); padding: 3px 8px;
  font-size: 11.5px; font-weight: 500; line-height: 1.35; letter-spacing: 0;
  white-space: nowrap; max-width: 15rem; overflow: hidden; text-overflow: ellipsis;
  box-shadow: var(--shadow);
  opacity: 0; visibility: hidden;
  transition: opacity .12s ease, transform .12s ease;
}
.badge-chip:hover .badge-name,
.badge-chip:focus-visible .badge-name {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .badge-name { transition: none; }
}

/* ── How big a badge is ─────────────────────────────────────────────────────
   TWO NUMBERS THAT MULTIPLY, which is group 11 in one declaration. `--badge-size` is the
   badge's own, one of six named sizes set by the class below; `--badge-scale` is the
   theme's, a whole number of percent written by shared/themes.js. Both default to "leave
   it exactly as it was", so a site that has set neither renders the 11.5px chip that was
   here before.

   The division by 100 is why the theme stores `120` rather than `120%`: calc() multiplies
   a length by a number and divides by a number, and will do neither with a percentage.
   Everything else in the chip is written in em, so padding, gap and the icon follow the
   font size instead of needing five more multiplications. */
.badge-chip {
  --badge-mult: calc(var(--badge-size, 1) * var(--badge-scale, 100) / 100);
  font-size: calc(11.5px * var(--badge-mult));
  padding: 0.22em 0.78em;
  gap: 0.35em;
}
.badge-chip.bs-tiny { --badge-size: .8; }
.badge-chip.bs-small { --badge-size: .9; }
.badge-chip.bs-medium { --badge-size: 1.15; }
.badge-chip.bs-large { --badge-size: 1.45; }
.badge-chip.bs-xlarge { --badge-size: 1.85; }

/* The icon inside a scaled badge. `!important` because iconMarkup writes the size inline —
   it has to, since every other icon on the site is a fixed pixel size chosen by whatever
   drew it — and an inline style is the one thing a class cannot outrank. Two declarations
   with a reason beats teaching every caller of iconMarkup about CSS lengths.

   1.6em rather than the 1.13em it was: the icon is the whole content of the square now, so
   it fills about three quarters of it rather than half. Measured rather than guessed — at
   1.35em a glyph sat in the middle of a lot of nothing, because a character’s em box is
   mostly air and the drawn mark is smaller again than the box it is given. `vertical-align` is reset because the chip centres with flex and
   the icon's old -0.15em nudge was there to sit it on a text baseline that has gone. */
.badge-chip .icon-img {
  width: 1.6em !important; height: 1.6em !important; vertical-align: 0;
}
.badge-chip .icon-glyph { font-size: 1.6em !important; vertical-align: 0; }

/* One step of an uploaded icon in the admin editor, under its own width in pixels. Each is
   drawn at its real size (up to 48px) so the strip shows what the steps actually are rather
   than four copies of one apparent size. */
.icon-step { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }

/* Visually hidden, still read aloud and still found by in-page search. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* A praise chip carries its own count, which is the number a badge rule fires on. The
   count sits in a pill inside the chip so it reads as part of the thing rather than as a
   separate control somebody might try to click. */
/* A card an operator has told a badge to highlight. Deliberately a border and a tint
   rather than anything that moves or animates — a list where some rows demand attention
   more loudly than others stops being a ranking and starts being an advert. */
.card-lit { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-line); }

/* The administrator's pencil, on every listing wherever it is drawn. Deliberately quiet —
   it sits beside a diner's own rating chip on a card most people are reading rather than
   editing, and an edit control that competes with the content is a worse card for everyone
   who is not an administrator. */
.admin-pencil { padding: 2px 7px; font-size: 13px; line-height: 1.4; opacity: 0.65; }
.admin-pencil:hover, .admin-pencil:focus { opacity: 1; }

/* The rest of one tag list, inside its dialog. Capped and scrollable rather than as long
   as the vocabulary happens to be: a house list of two hundred cuisines would otherwise
   push Done off the bottom of a phone, which is the one control that has to be reachable. */
.tag-picker-list { max-height: 46vh; overflow-y: auto; align-content: flex-start; padding: 2px 2px 6px; }
/* "+12 more" — a chip because it sits in a row of chips, dashed because it is a way in
   rather than a value you can be filtering on. */
.chip.more { border-style: dashed; font-weight: 600; }

/* Fixing a listing from wherever you saw it. A dialog over the page rather than a
   navigation, because the point is not to lose your place in the list you were reading. */
.quick-edit-backdrop {
  position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: flex-start; justify-content: center;
  padding: 24px 16px; overflow-y: auto;
  background: rgba(0, 0, 0, 0.45);
}
.quick-edit { width: 100%; max-width: 620px; margin: auto; }
@media (max-width: 640px) { .quick-edit-backdrop { padding: 0; } .quick-edit { border-radius: 0; min-height: 100%; } }

/* The colour boxes on a theme page. The swatch is repainted on every keystroke by the
   input listener — a style property on one element, never a re-render of the box being
   typed into. The chequerboard shows through when a field is empty, which is what "this
   theme sets no colour here, the palette does" ought to look like. */
.swatch {
  width: 22px; height: 22px; flex: 0 0 22px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background-image:
    linear-gradient(45deg, var(--surface-2) 25%, transparent 25%),
    linear-gradient(-45deg, var(--surface-2) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--surface-2) 75%),
    linear-gradient(-45deg, transparent 75%, var(--surface-2) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
}
.swatch-row { align-items: center; }
.swatch-row input { font-family: var(--mono); font-size: 13px; }

.praise-chip { display: inline-flex; align-items: center; gap: 5px; }
.praise-count {
  display: inline-block; min-width: 1.4em; padding: 0 4px; border-radius: 999px;
  background: var(--surface-2); font-size: 0.78em; font-variant-numeric: tabular-nums;
  text-align: center;
}
.praise-chip.on .praise-count { background: rgba(255, 255, 255, 0.25); }

.icon-choices { max-height: 180px; overflow-y: auto; align-content: flex-start; }
.icon-choices .chip { display: inline-flex; align-items: center; gap: 4px; min-height: 30px; }
.r-thumb {
  width: 100%; height: 132px; object-fit: cover; border-radius: var(--radius-sm);
  border: 1px solid var(--line); display: block;
}

/* ── Table-side redemption ──────────────────────────────────────────────── */
.redeem-panel { max-width: 520px; }
.scan-btn { padding: 15px; font-size: 15px; font-weight: 600; }
#scan-video { width: 100%; max-height: 260px; border-radius: var(--radius); background: #000; object-fit: cover; }

.verify-panel {
  border-radius: var(--radius); padding: 18px; margin-top: 12px;
  display: flex; flex-direction: column; gap: 9px; text-align: center;
  border: 2px solid transparent;
}
.verify-panel.ok { background: var(--good-soft); border-color: var(--good); }
.verify-panel.done { background: var(--good-soft); border-color: var(--good); }
.verify-panel.bad { background: var(--bad-soft); border-color: var(--bad); }
.verify-verdict {
  font: 700 15px/1 var(--body); letter-spacing: .08em; text-transform: uppercase;
}
.verify-panel.ok .verify-verdict, .verify-panel.done .verify-verdict { color: var(--good); }
.verify-panel.bad .verify-verdict { color: var(--bad); }
.verify-amount {
  font-family: var(--mono); font-size: 42px; font-weight: 700; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.verify-panel .row { justify-content: center; }

/* ── Palette picker ─────────────────────────────────────────────────────── */
.palette-row { display: flex; gap: 10px; flex-wrap: wrap; }
.palette-swatch {
  display: flex; align-items: center; gap: 8px; padding: 8px 13px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); cursor: pointer; font-size: 13px;
}
.palette-swatch.on { border-color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.palette-dot { width: 14px; height: 14px; border-radius: 50%; flex: none; }

/* ── Announcement banner ─────────────────────────────────────────────────────
   Sits above the shell in normal flow rather than fixed over it. An announcement that
   overlays the page covers content on short screens and has to be compensated for
   everywhere; one that pushes the page down cannot.
   ──────────────────────────────────────────────────────────────────────────── */
.banner {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 18px; font-size: 13.5px; line-height: 1.45;
  border-bottom: var(--border-w) solid var(--line);
  background: var(--surface-2); color: var(--ink-2);
}
.banner-text { flex: 1; min-width: 0; }
.banner b { font-weight: 700; }
.banner-close {
  border: 0; background: none; cursor: pointer; color: inherit; opacity: .6;
  font-size: 17px; line-height: 1; padding: 0 2px; flex: none;
}
.banner-close:hover { opacity: 1; }
.banner.info { background: var(--accent-soft); color: var(--accent); border-bottom-color: var(--accent-line); }
.banner.warn { background: var(--warn-soft); color: var(--warn); border-bottom-color: transparent; }
.banner.alert { background: var(--bad-soft); color: var(--bad); border-bottom-color: transparent; }
.banner.good { background: var(--good-soft); color: var(--good); border-bottom-color: transparent; }

/* ── Day / night switch ──────────────────────────────────────────────────────
   A real control with a visible current state, not a mystery icon: which of the three
   options is active is legible without clicking anything.
   ──────────────────────────────────────────────────────────────────────────── */
.modeswitch {
  display: inline-flex; border: var(--border-w) solid var(--line-strong);
  border-radius: var(--radius-sm); overflow: hidden; background: var(--surface);
}
.modeswitch button {
  border: 0; background: none; cursor: pointer; padding: 4px 9px;
  font-size: 12px; color: var(--muted); border-right: 1px solid var(--line);
  display: inline-flex; align-items: center; gap: 5px;
}
.modeswitch button:last-child { border-right: 0; }
.modeswitch button:hover { background: var(--surface-2); color: var(--ink); }
.modeswitch button.on { background: var(--accent); color: var(--accent-ink); font-weight: 600; }

/* ── Theme picker ───────────────────────────────────────────────────────────── */
.theme-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); }
.theme-card {
  display: flex; gap: 11px; align-items: flex-start; text-align: left; cursor: pointer;
  padding: 12px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface); color: inherit;
}
.theme-card:hover { border-color: var(--line-strong); }
.theme-card.on { border-color: var(--accent); background: var(--accent-soft); }
/* The swatch previews the theme's own display face, so the choice is visible up front. */
.theme-swatch {
  flex: none; width: 38px; height: 38px; display: grid; place-items: center;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  font-size: 17px; font-weight: 600; background: var(--surface-2);
}
.theme-card b { font-size: 13.5px; display: block; }
.theme-card span { font-size: 11.5px; color: var(--muted); line-height: 1.4; display: block; margin-top: 2px; }

.swatch-editorial { font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif; }
.swatch-diner { font-family: "SF Pro Rounded", "Nunito", "Varela Round", "Trebuchet MS", sans-serif; font-weight: 700; }
.swatch-market { font-family: Inter, "Segoe UI", Roboto, system-ui, sans-serif; letter-spacing: -.03em; }
.swatch-supper { font-family: Didot, "Bodoni MT", "Hoefler Text", Garamond, serif; }
.swatch-zine { font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace; font-weight: 700; }

/* ── Copy editor ────────────────────────────────────────────────────────────── */
.copy-row {
  display: grid; grid-template-columns: 210px 1fr; gap: 12px; align-items: start;
  padding: 9px 0; border-top: 1px solid var(--line);
}
.copy-row:first-of-type { border-top: 0; }
.copy-row textarea { min-height: 0; height: 34px; }
.copy-row .copy-default { font-size: 11px; color: var(--muted); margin-top: 3px; }
.copy-row .copy-src { font-size: 10.5px; }
@media (max-width: 700px) { .copy-row { grid-template-columns: 1fr; gap: 5px; } }

/* One collapsible block per page on the Wording screen (2026-09-10). The summary is the
   whole header row, so the target is wide; the chevron turns rather than swapping glyphs. */
.copy-group > summary {
  display: flex; align-items: baseline; gap: 10px; cursor: pointer;
  list-style: none; user-select: none;
}
.copy-group > summary::-webkit-details-marker { display: none; }
.copy-group > summary::before {
  content: '\25B8'; color: var(--muted); display: inline-block;
  transition: transform .15s ease;
}
.copy-group[open] > summary::before { transform: rotate(90deg); }
.copy-group > summary .eyebrow { margin: 0; }
.copy-group > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.copy-group .copy-changed { color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .copy-group > summary::before { transition: none; } }

.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  background: var(--ink); color: var(--bg); padding: 9px 16px; border-radius: 999px;
  font-size: 13px; z-index: 90; box-shadow: var(--shadow); animation: rise .25s ease-out;
}
@keyframes rise { from { opacity: 0; transform: translate(-50%, 8px); } }

.modal-scrim {
  position: fixed; inset: 0; background: rgba(20,14,18,.5); z-index: 80;
  display: grid; place-items: center; padding: 18px; overflow-y: auto;
}
.modal { background: var(--surface); border-radius: 12px; border: 1px solid var(--line); max-width: 560px; width: 100%; box-shadow: var(--shadow); }

canvas { display: block; max-width: 100%; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* Long lists (2026-09-18): the find box hides rows with the hidden attribute, and several
   layouts set display on rows and cards, so say it again. */
[data-findable] > [hidden], tr[hidden] { display: none !important; }
.find-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 4px 0 8px; }
.find-bar .find-input { flex: 1 1 220px; min-width: 0; }
.find-bar [data-act="find-all"][hidden] { display: none; }
.select-find { display: block; width: 100%; margin: 0 0 4px; font-size: 14px; padding: 5px 8px; }

/* A listing's menu: each dish and its photos are one row, so the find box keeps them together. */
.dish-item > .dish { border-top: 1px solid var(--line); }
.dish-item:first-child > .dish { border-top: 0; }
