:root {
  /* WHITE, BLACK, AND BURGUNDY. Robin, 2026-10-07: white background,
     burgundy buttons, black text on white and white text on burgundy.
     One palette — the dark theme is gone, not overridden. It followed
     prefers-color-scheme and nothing ever set `data-theme`, so the
     [data-theme="dark"] block beside it was already unreachable.

     #8a1f2b measures 9.08:1 both ways: white on it, and it as text on white.
     So the same token is a button fill and a link colour without either one
     being a compromise, which is what the old palette could not do — its
     dark accent passed as text and failed as a fill, and vice versa. */
  --bg: #ffffff;
  --surface: #ffffff;
  /* A raised row needs to be visible against a pure white page without
     becoming a grey box. Warm, because the burgundy and the logo are warm. */
  --surface-2: #f5f2f0;
  --text: #000000;
  --text-dim: #5c5653;
  --line: #e3ddda;
  --accent: #8a1f2b;
  --accent-ink: #ffffff;
  --accent-2: #246c24;      /* the logo's green */
  --star: #b8760a;          /* the cheese yellow, darkened to carry on white */
  --radius: 14px;
  --shadow: 0 1px 2px rgba(0,0,0,.05), 0 8px 24px rgba(0,0,0,.06);
}

* { box-sizing: border-box; }

/* Added for the home-screen app, where the tab bar came unstuck on every
   screen except the feed.
   
   Standalone iOS has no browser chrome to absorb an overscroll, so a
   document SHORTER than the viewport still rubber-bands — and a
   `position: fixed` bar rides that bounce and settles away from the
   bottom. The feed hid it because the feed is the one screen long enough
   to scroll for real.
   
   So: every screen is at least a viewport tall, which removes the short
   document entirely, and the bounce is turned off. `dvh` after `vh` so
   the dynamic toolbar is accounted for where it is supported and ignored
   where it is not. */
html { height: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  overscroll-behavior-y: none;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 ui-rounded, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  padding-bottom: env(safe-area-inset-bottom);
}

.wrap { max-width: 620px; margin: 0 auto; padding: 0 16px; }

header.bar {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--line);
  padding: max(10px, env(safe-area-inset-top)) 0 10px;
}
.bar .wrap { display: flex; align-items: center; gap: 10px; }

.mark { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: -.02em; }
.mark svg { width: 26px; height: 26px; flex: none; }
.mark small { display: block; font-weight: 500; font-size: 11px; color: var(--text-dim); letter-spacing: 0; }

.spacer { flex: 1; }

/* SQUARED AT THE SOURCE. Robin's call, 2026-10-07: every pill-shaped button
   takes the shape the rest of the site uses. This was 999px, so each screen
   that wanted the squared look had to override it — four separate rules had
   piled up doing exactly that, and they are gone now because this is the
   default rather than the exception.

   12px here and 10px on `.small` below: a small button is about 32px tall, so
   12px leaves its corners almost meeting and still reads as the lozenge it
   was. Different numbers, one family.

   Badges keep 999px, because they are not buttons: `.pill`, `.badge`, `.rec`
   and `.tile-likes` are spans saying Sponsored or a like count, and
   `.press-hint` is an overlay with `pointer-events: none`. */
button {
  font: inherit; cursor: pointer; border: 0; border-radius: 12px;
  /* OFF-WHITE BY DEFAULT, not the accent fill. Robin's call, 2026-10-07, and
     this is the line that actually settles it: every bare `button` inherited
     an accent fill, which is why Cook this, Save recipe, Post, Add to menu,
     Claim it and the rest kept turning up maroon one at a time. They were not
     separate problems — they were this one, seen from eight screens.

     Identical to `.ghost` below, which is now a no-op kept for intent. No
     hairline: `.ghost` never had one and the page has read fine without it,
     and a `box-shadow` here would draw a rectangle around `.tb`, `.linky` and
     every other button that sets `background: none`.

     The accent survives on interactive things ONLY where it means selected or
     pressed — `.days button.on`, `.azk.on`, `.dir a:active` — which is the
     same job it does on `.tab.on`. It is no longer what "this is a button"
     looks like. */
  padding: 10px 18px; background: var(--surface-2); color: var(--text);
  font-weight: 600;
  /* THE HAIRLINE IS NOT DECORATION. `--surface-2` is #f5f2f0 and `--bg` is
     #ffffff — a 2% difference — so without it an off-white button on a white
     page is very nearly invisible, and the splash's Allow Location Services
     is the first thing a new visitor sees. `.tab`, `.recact` and the post disc
     all already carry exactly this line. */
  box-shadow: inset 0 0 0 1px var(--line);
  transition: transform .08s ease, opacity .15s ease, box-shadow .15s ease;
}

/* THE BUTTONS THAT MUST NOT HAVE IT, enumerated rather than discovered one
   screen at a time. Two kinds: those that set `background: none` and are
   therefore not a surface at all, and those that already draw their own
   border. A hairline on the first kind outlines a transparent box; on the
   second it doubles the line.
   `.tab`, `.viewtog .vt`, `.recact` and `.tb-post svg` are absent on purpose
   — each sets its own box-shadow and so overrides this already. */
.act button,
.sheet-item,
.pile button,
.linky,
.tb,
button.google,
.place-pick,
.here-pick,
.first-go { box-shadow: none; }
button:active { transform: scale(.97); }
button:disabled { opacity: .45; cursor: default; }
button.ghost { background: var(--surface-2); color: var(--text); }
button.small { padding: 7px 13px; font-size: 14px; border-radius: 10px; }

/* A button that is only an icon. Square padding so it reads as a round
   target rather than a pill with nothing written in it. */
/* ROUND, DELIBERATELY, and the one exception to the squaring above. The note
   below is the reason: this is a target with nothing written in it, so a
   circle reads as a target where a rounded square reads as an empty button.
   It carries `.small`, hence the override. */
button.icon-btn {
  padding: 7px; display: inline-flex; border-radius: 999px;
}
button.icon-btn svg { width: 19px; height: 19px; display: block; }
/* Turning while it works, so a tap that takes a second to get a GPS fix
   does not look like a tap that did nothing. */
button.icon-btn.spinning svg { animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

input, textarea {
  font: inherit; width: 100%; padding: 13px 14px; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); outline: none;
}
input:focus, textarea:focus { border-color: var(--accent); }
textarea { resize: vertical; min-height: 78px; }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  overflow: hidden; margin: 14px 0;
}
.card .body { padding: 14px; }
/* 4:3, same as it always was — and now the same shape the file is stored in.
 * The composer crops to this before it stamps the badge, so a card shows the
 * whole stored image and the badge is in a corner somebody can see. Nothing is
 * cropped twice. */
.card img.plate { width: 100%; display: block; aspect-ratio: 4/3; object-fit: cover; background: var(--surface-2); }

.row { display: flex; align-items: center; gap: 10px; }
.muted { color: var(--text-dim); font-size: 14px; }
h1 { font-size: 22px; letter-spacing: -.02em; margin: 18px 0 4px; }
h2 { font-size: 17px; letter-spacing: -.01em; margin: 0 0 4px; }

.pill {
  display: inline-block; padding: 3px 9px; border-radius: 999px;
  background: var(--surface-2); font-size: 12px; font-weight: 600; color: var(--text-dim);
}
.pill.live { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }

.empty { text-align: center; padding: 56px 20px; color: var(--text-dim); }
.empty svg { width: 46px; height: 46px; opacity: .35; margin-bottom: 10px; }

.fab {
  position: fixed; right: max(18px, calc(50% - 310px + 18px));
  bottom: calc(20px + env(safe-area-inset-bottom));
  width: 58px; height: 58px; padding: 0; border-radius: 50%;
  box-shadow: 0 6px 20px rgba(168,48,12,.38); z-index: 30;
  display: grid; place-items: center;
}
.fab svg { width: 26px; height: 26px; }

dialog {
  border: 0; border-radius: 18px; padding: 0; width: min(520px, 94vw);
  background: var(--surface); color: var(--text); box-shadow: var(--shadow);
}
dialog::backdrop { background: rgba(0,0,0,.45); backdrop-filter: blur(3px); }
dialog .body { padding: 18px; }

.stack > * + * { margin-top: 12px; }
.note { font-size: 13px; color: var(--text-dim); }
.err { color: #d1442a; font-size: 14px; }

.code {
  font: 600 15px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--surface-2); padding: 10px 12px; border-radius: 10px;
  word-break: break-all;
}
/* The staff code on the Sponsored tab. Bigger than a .code elsewhere because
   it is read off a screen and typed on somebody else's phone, often across a
   table — see the owner panel. Letter-spaced so five characters read as five
   characters rather than a word.

   (It is not to separate O from 0: the alphabet in restaurants/staffCode.ts
   contains neither, nor 1, I or L, for exactly that reason. The spacing is
   for legibility at a distance, not disambiguation.) */
.staff-code {
  font-size: 30px; letter-spacing: .18em; text-align: center;
  padding: 14px 12px; margin: 4px 0 2px;
}

/* THE 16px SIDE GUTTER HAS TO BE REPEATED HERE. `#tabs` is `class="wrap tabs"`,
   and `.wrap` sets `padding: 0 16px` — but this rule is a single class too and
   is defined later, so its own `padding` wins outright and zeroes those
   gutters. That was invisible while the pills were content-sized and
   left-aligned; the moment they became `flex: 1` they filled the extra 32px
   and hung past every card below them. */
.tabs {
  display: flex; gap: 8px; padding: 18px 16px 4px; align-items: stretch;
}
/* FOUR PILLS ON A NARROW PHONE. The TRENDING masthead and its .tab-head rule
   went on 2026-10-07 to make room for Cafes — at the old 16px padding and 15px
   text, four of these came to about 343px and a 360px phone has 328 between the
   gutters, so they wrapped. Tightened rather than allowed to wrap: the row is a
   choice of four and reads as one line or not at all. */
.tab {
  position: relative;
  isolation: isolate;
  background: var(--surface-2);
  color: var(--text-dim);
  /* FOUR EQUAL SHARES OF A PLATE'S WIDTH. Robin, 2026-10-07: centre the group,
     make them squarer, and expand them to the length of a plate in the feed.

     `flex: 1` is all "centre" needs here — four equal shares of the row fill
     it, so the group is centred by occupying the whole thing. `#tabs` and
     `#main` are both `.wrap`, so that width IS a plate's width with no number
     to keep in step.

     This retires a measurement that used to govern: the old note said four
     pills came to ~343px against 328px of usable width on a 360px phone, which
     is why the padding was tightened and why the hairline is an inset shadow
     rather than a border. Sized by the container instead of by their own text,
     none of that binds — but the shadow stays, because a border would still
     add 8px of layout across the row and `flex: 1` would simply shrink the
     labels to pay for it. */
  flex: 1;
  min-width: 0;
  padding: 11px 6px;
  font-size: 14px;
  font-weight: 600;
  /* SQUARER. `button` sets 999px globally, which on a wide button reads as a
     lozenge rather than a control. The chase ring is `border-radius: inherit`,
     so it follows this without being told. */
  border-radius: 12px;
  box-shadow: inset 0 0 0 1px var(--line);
  transition: color .15s ease, background .15s ease, box-shadow .15s ease;
}
.tab:not(.on):hover {
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--text-dim);
}
.tab.on {
  /* The same treatment as `.viewtog .vt.on`: no fill, a bright label, and the
     chase ring doing the work of saying which one is open. Two accent cues on
     one control was one too many, and four burgundy slabs across the top of
     the feed read as a toolbar rather than a choice of four. */
  background: var(--surface-2);
  color: var(--text);
  box-shadow: none;
}

/* ---------------------------------------------------------------------------
 * The light chase on the pill you are on.
 *
 * Ported from CashCode's `.landing button`, where the note that matters is the
 * one about restraint: "It carries meaning rather than decoration, which is
 * why it is not on every card." So it runs on the ACTIVE pill only. Four
 * rings chasing at once is a loading screen, not a choice of four; one ring
 * says which feed you are looking at.
 *
 * `@property` is the whole trick. A plain custom property is not
 * interpolatable, so the conic gradient's angle would jump between keyframes
 * rather than sweep. Registering --a as an <angle> gives the browser a type it
 * can tween.
 *
 * BURGUNDY, ON WHITE. Robin, 2026-10-07: make the four pills like the
 * Plates/Restaurants pair — white, with the chase border saying which one is
 * open. So the active pill lost its solid `--accent` fill and the ring took
 * the accent instead.
 *
 * That makes one rule serve both controls with no override. It was white
 * before for CashCode's reason — a sweep the same colour as the fill beneath
 * it is invisible — and with the fill gone there is nothing for white to show
 * up against on a white page.
 * ------------------------------------------------------------------------- */
@property --a {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@keyframes spin-border {
  to { --a: 360deg; }
}

.tab.on::before,
.tab.on::after,
.viewtog .vt.on::before,
.viewtog .vt.on::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 2px;
  background: conic-gradient(
    from var(--a),
    transparent 0deg 200deg,
    var(--chase-a, color-mix(in srgb, var(--accent) 65%, #ffffff)) 280deg,
    var(--chase-b, var(--accent)) 320deg,
    transparent 360deg
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  animation: spin-border 3s linear infinite;
  pointer-events: none;
}

/* The bloom, from a blurred copy of the same sweep. A pill is small, so this
   is tighter than CashCode's card version — at a full-size blur the halo
   swamps a 14px label. The negative inset is what keeps it a halo rather than
   a hard edge clipped to the pill's own box. */
/* The ring and the bloom are the same burgundy now. They had to differ while
   the active pill was a solid `--accent` fill — a white ring to read against
   it, an accent bloom to read against the white page behind. With the fill
   gone both halves sit on paper, so both take the accent and the override
   that used to split them is just the inherited default. Left as inset and
   blur only. */
.tab.on::after,
.viewtog .vt.on::after {
  inset: -2px;
  filter: blur(6px);
  opacity: .5;
  z-index: -1;
}


/* Honour the setting. A ring that never stops moving is exactly what this
   asks to be turned off, and the pill still reads as chosen without it. */
@media (prefers-reduced-motion: reduce) {
  .tab.on::before, .tab.on::after,
  .viewtog .vt.on::before, .viewtog .vt.on::after { animation: none; }
  .tab.on::after, .viewtog .vt.on::after { opacity: .35; }
}

.act { display: flex; gap: 6px; align-items: center; margin-top: 10px; }
.act button {
  background: none; color: var(--text-dim); padding: 6px 8px;
  display: inline-flex; align-items: center; gap: 6px; font-size: 14px;
}
.act button.on { color: var(--accent); }
.act svg { width: 19px; height: 19px; }

.cmt { padding: 9px 0; border-bottom: 1px solid var(--line); }
.cmt:last-child { border-bottom: 0; }
.cmt b { font-size: 14px; }
.cmt p { margin: 2px 0 0; font-size: 15px; }
#cmtList { max-height: 46vh; overflow: auto; }

button.google {
  width: 100%; background: var(--surface); color: var(--text);
  /* 12px like every other button. `var(--radius)` is 14px and belongs to
     inputs; this control sat on it and so was missed when `button` was
     squared at the source. */
  border: 1px solid var(--line); border-radius: 12px;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 13px 16px;
}
button.google svg { width: 19px; height: 19px; }

.or {
  display: flex; align-items: center; gap: 12px;
  color: var(--text-dim); font-size: 13px; margin: 4px 0;
}
.or::before, .or::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}

dialog.sheet {
  width: min(460px, 100vw); margin: auto auto 0; border-radius: 18px 18px 0 0;
  animation: rise .18s ease-out;
}
@media (min-width: 520px) {
  dialog.sheet { margin: auto; border-radius: 18px; }
}
@keyframes rise { from { transform: translateY(14px); opacity: 0 } to { transform: none; opacity: 1 } }
dialog.sheet .body { padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); }

.sheet-item {
  width: 100%; background: none; color: var(--text);
  display: flex; align-items: center; gap: 12px;
  padding: 15px 14px; border-radius: 12px; font-weight: 500;
  justify-content: flex-start; text-align: left;
}
.sheet-item:active { background: var(--surface-2); }
.sheet-item svg { width: 21px; height: 21px; flex: none; color: var(--text-dim); }
.sheet-item.danger { color: #d1442a; }

/* A title on a sheet that is a chooser rather than a list of actions. */
.sheet-head {
  margin: 4px 14px 6px; font-size: 13px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim);
}
/* The second half of a chooser row: what the thing actually is. Pushed to
   its own line so the row reads name-then-explanation at a glance. */
.sheet-sub {
  display: block; font-size: 13px; font-weight: 400; color: var(--text-dim);
  margin-top: 1px;
}
.sheet-item .sheet-sub { margin-left: 0; }
/* A warmer red than the burgundy, so "delete" never reads as just another
   accent-coloured row. Kept distinct from --accent on purpose. */
.sheet-item.danger svg { color: #d1442a; }
.sheet-item.cancel {
  justify-content: center; background: var(--surface-2);
  margin-top: 6px; font-weight: 600;
}

.card.pressing { transform: scale(.985); transition: transform .12s ease; }

.reason {
  width: 100%; justify-content: flex-start; text-align: left;
  background: var(--surface-2); color: var(--text);
  border-radius: 12px; padding: 13px 14px; margin-bottom: 8px; font-weight: 500;
}

/* The logo is a badge with the wordmark baked in, so the adjacent text
   stays small — it carries the tagline and keeps the name legible at
   sizes where the badge's own lettering is not. */
.mark img {
  width: 38px; height: 38px; flex: none; display: block;
}

/* ---------------------------------------------------------------
   Feed slots.

   A house card is an invitation, not a post — so it deliberately does
   not imitate one. Centred, warm, no author line, no like or comment
   row. Someone scrolling should never mistake it for something a
   person made.
   --------------------------------------------------------------- */
.card.promo.house {
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 22%, var(--line));
  cursor: pointer;
}
.card.promo.house .body {
  display: flex; flex-direction: column; align-items: center;
  gap: 10px; padding: 26px 18px;
}
.card.promo.house img { display: block; }
.card.promo.house .line {
  margin: 0; font-weight: 600; font-size: 17px;
  letter-spacing: -.01em; color: var(--accent);
}
.card.promo.house:active { transform: scale(.99); }

/* ---------------------------------------------------------------
   Choosing where you are.
   --------------------------------------------------------------- */
.place-pick {
  width: 100%; justify-content: flex-start; text-align: left;
  background: var(--surface-2); color: var(--text-dim);
  /* 12px, same reason as button.google: this is a tappable control, not an
     input, and `var(--radius)` kept it out of the squaring pass. */
  border: 1px dashed var(--line); border-radius: 12px;
  display: flex; align-items: center; gap: 10px; padding: 13px 14px;
  font-weight: 500;
}
.place-pick svg { width: 20px; height: 20px; flex: none; }
/* Chosen state: solid, accent-coloured, so the required step reads as
   done at a glance rather than needing to be re-read. */
.place-pick.chosen {
  border-style: solid;
  border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
  color: var(--accent); font-weight: 600;
}

#placeList { max-height: 46vh; overflow: auto; margin: 0 -4px; }
.place-row {
  width: 100%; justify-content: flex-start; text-align: left;
  background: none; color: var(--text); border-radius: 12px;
  padding: 12px 10px; display: block;
}
.place-row:active { background: var(--surface-2); }
/* The row is a <button> in the app and an <a> in the admin console's kitchen
   list, where a real link is wanted so it can be opened in its own tab. The
   global `a` rule underlines; a row is a row either way. */
a.place-row { text-decoration: none; }
.place-row b { display: block; font-size: 16px; }
.place-row span { display: block; font-size: 13px; color: var(--text-dim); margin-top: 2px; }

.attrib { text-align: center; font-size: 12px; opacity: .75; }

/* The restaurant a post came from, under the author. */
.card .where {
  display: inline-block; margin-top: 2px; font-size: 13px;
  color: var(--accent); font-weight: 600; text-decoration: none;
}

/* Where in the world, under the restaurant name on the Social feed. Quieter
   than the name and on its own line: it is context for the name above it, not
   a second thing to read. Clipped rather than wrapped, because a long "City,
   Country" must not push a card taller than its neighbours. */
.card .where-place {
  display: block; margin-top: 1px;
  font-size: 12px; color: var(--muted);
  max-width: 100%; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}

/* "You're at X" banner above the feed. */
.here {
  display: flex; align-items: center; gap: 10px; width: 100%;
  text-align: left; justify-content: flex-start;
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
  border-radius: var(--radius); padding: 13px 14px; margin-top: 14px;
  color: var(--text);
}
.here svg { width: 20px; height: 20px; flex: none; color: var(--accent); }
.here b { color: var(--accent); }

/* Restaurant page header. */
.rhead { padding: 18px 0 6px; }
.rhead h1 { margin: 0 0 4px; }
.rhead .meta { color: var(--text-dim); font-size: 14px; }

/* A place you are not actually at: visible, so you know it's there, but
   not selectable — the gate is explained by the row itself rather than by
   an error after you tap. */
.place-row.far { opacity: .5; }
.place-row.far b { font-weight: 500; }
.far-note { color: var(--text-dim); font-style: italic; }

.owner-tag {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 13px; font-weight: 600; color: var(--accent-2, var(--accent));
}
.owner-tag svg { width: 15px; height: 15px; }

/* Feed / Menu switch on a restaurant page. */
.rtabs { display: flex; gap: 8px; margin: 4px 0 2px; }

/* A dish: its real photograph, and how many people shot it. */
.dish {
  display: flex; gap: 12px; align-items: center; width: 100%;
  text-align: left; justify-content: flex-start;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 10px; margin-top: 10px;
  color: var(--text); box-shadow: var(--shadow);
}
.dish img {
  width: 72px; height: 72px; border-radius: 10px; object-fit: cover;
  flex: none; background: var(--surface-2);
}
.dish .d-name { font-weight: 600; font-size: 16px; }
.dish .d-meta { font-size: 13px; color: var(--text-dim); margin-top: 3px; }
.dish .d-done { color: var(--accent-2, var(--accent)); font-weight: 600; }

.sect-head {
  margin: 20px 0 2px; font-size: 13px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim);
}

/* The owner's untagged pile. */
.pile { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
.pile button { padding: 0; background: none; border-radius: 10px; overflow: hidden; }
.pile img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }

#assignList { max-height: 34vh; overflow: auto; }
.progress {
  font-size: 14px; color: var(--text-dim); margin: 8px 0 0;
}

/* ---------------------------------------------------------------
   Splash — what a stranger sees before signing in.

   It exists because a cold visitor in a town with no posts would
   otherwise land on an empty feed, which explains nothing and invites
   nobody. Mobile-first: this is nearly always read on a phone, often
   standing up, seconds after scanning something.
   --------------------------------------------------------------- */
#splash { padding-block: 8px 48px; }

.splash-in {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.splash-mark {
  width: 132px; height: 132px; display: block;
  margin: 10px 0 18px;
}

.splash-h {
  font-size: clamp(28px, 8.5vw, 38px);
  line-height: 1.12;
  letter-spacing: -.025em;
  margin: 0 0 12px;
  text-wrap: balance;
}

.splash-sub {
  margin: 0 0 26px;
  font-size: 17px;
  line-height: 1.5;
  color: var(--text-dim);
  max-width: 34ch;
  text-wrap: pretty;
}

.splash-cta {
  display: flex; flex-direction: column; gap: 10px;
  width: 100%; max-width: 320px;
}
.splash-cta button { width: 100%; padding: 15px 20px; font-size: 16px; }

/* The three beats are numbered because they are a sequence: what gets in,
   who may add to it, who may look. Not decoration. */
.beats {
  list-style: none; margin: 34px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 18px;
  text-align: left; max-width: 420px; width: 100%;
}
.beats li { display: flex; gap: 14px; align-items: flex-start; }
.beats b { display: block; margin-bottom: 2px; }
.beats div { font-size: 15px; line-height: 1.5; color: var(--text-dim); }
.beats b { color: var(--text); }

.beat-n {
  flex: none;
  width: 27px; height: 27px; border-radius: 50%;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  color: var(--accent);
  font: 700 13px/1 ui-sans-serif, -apple-system, system-ui, sans-serif;
  margin-top: 1px;
}

.splash-foot {
  margin: 34px 0 0; font-size: 14px; font-weight: 600;
  color: var(--accent); letter-spacing: -.01em;
}

/* The mark is the way home. It was a div, which meant the one thing
   everyone taps to get back did nothing. */
/* LINKS ARE TEXT, NOT BROWSER DEFAULTS.
 *
 * There was no `a` rule at all, so every unclassed link — the terms and
 * privacy links in the footers, the mailto addresses, the links inside both
 * legal pages — rendered in the browser's own blue, and purple once visited.
 * On a warm paper-and-ink palette that reads as a 1998 page somebody forgot to
 * style, and the purple makes "have you clicked this" the loudest thing in a
 * paragraph.
 *
 * So a link is the same colour as the sentence it sits in, and says it is a
 * link by being underlined. The underline is offset because a tight one
 * collides with the descenders in a serif-ish face at this size.
 *
 * `currentColor` rather than `var(--text)`: a link inside dimmed copy should
 * be dim, and a link inside an accent-coloured block should be that accent.
 * Anything with its own class still wins, as it did before. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  text-underline-offset: 2px;
}
a:active { color: var(--accent); }
/* Visited is deliberately not styled: on a feed, "seen before" is noise. */
a:visited { color: inherit; }

a.mark { text-decoration: none; color: inherit; }
a.mark:active { opacity: .7; }

/* A quiet way back to the explainer, so dismissing it once is not
   permanent, and beside it the only way into /owners from inside the app.
   The bottom padding clears the fixed tab bar. */
#aboutFoot {
  display: flex; justify-content: center; gap: 22px;
  padding-block: 26px 40px;
  text-align: center;
}
.about-link {
  font-size: 14px; color: var(--text-dim); text-decoration: none;
  border-bottom: 1px solid var(--line); padding-bottom: 2px;
}
.about-link:active { color: var(--accent); }

/* Under the splash's sign-off, where somebody who came to read about Saucer
   rather than to eat will be by the time they have finished reading. Quiet
   on purpose: the splash is arguing for diners, and this must not compete
   with "Get started" — an owner is looking for it, not being sold it. */
.splash-owners { margin: 18px 0 2px; }

/* A quiet way out on a screen about money. Deliberately not a button: it must
   not compete with "Claim it — $100/year" sitting above it, and somebody who
   needs it is looking for it rather than being offered it. */
.linky {
  background: none; border: 0; padding: 6px 0;
  font-size: 13px; color: var(--text-dim);
  text-decoration: underline; align-self: flex-start;
}
.linky:active { color: var(--accent); }

/* --- the discount builder -------------------------------------------
   A row of controls that has to read as one sentence being assembled, so
   they sit on one line and share a baseline rather than stacking. */
.offer-row { display: flex; gap: 8px; align-items: center; }
.offer-row select,
.offer-row input {
  flex: 1; min-width: 0; padding: 12px 10px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--text); font: inherit; font-size: 15px;
}
.offer-row input[type="number"] { max-width: 92px; text-align: center; }
.offer-row .at { color: var(--text-dim); font-size: 14px; flex: none; }

.lbl .opt { color: var(--text-dim); font-weight: 400; font-size: 12px; }

/* Shortcuts beside the date field. They fill the date in rather than
   replacing it, so the restaurant always sees the actual day it lands on. */
#offExpires {
  padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line);
  background: var(--surface); color: var(--text); font: inherit; width: 100%;
}

/* Days of the week. Toggles, not a select: picking Friday and Saturday
   should be two taps, not two trips through a menu. */
.days { display: flex; gap: 6px; flex-wrap: wrap; }
.days button {
  flex: 1 1 auto; min-width: 44px; padding: 9px 6px; font-size: 13px;
  background: var(--surface-2); color: var(--text-dim); font-weight: 600;
  border-radius: 10px;
}
.days button.on { background: var(--accent); color: var(--accent-ink); }

/* The sentence the diner will read, shown while it is being built. */
.offer-preview {
  margin: 4px 0 0; padding: 12px 14px; border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
  font-size: 15px; font-weight: 600; letter-spacing: -.01em;
  min-height: 21px;
}
.offer-preview.bad {
  background: color-mix(in srgb, #d1442a 10%, var(--surface));
  border-color: color-mix(in srgb, #d1442a 35%, var(--line));
  font-weight: 500;
}

/* The checkbox that turns an advertisement into an invitation. Big tap
   target, explanation under the label rather than in a tooltip. */
.check {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 12px 13px; border-radius: 12px;
  background: var(--surface-2); cursor: pointer;
}
.check input { width: 20px; height: 20px; flex: none; margin-top: 1px; accent-color: var(--accent); }
.check span { font-size: 13px; line-height: 1.45; color: var(--text-dim); }
.check b { display: block; font-size: 15px; color: var(--text); margin-bottom: 2px; }

/* Claiming an offer off a sponsored plate. Full width because it is the
   only thing on that card a stranger can do. */
.claim-offer {
  display: block; width: calc(100% - 28px); margin: 0 14px 14px;
  padding: 13px; font-size: 15px;
}
.claim-offer.claimed {
  background: var(--surface-2); color: var(--accent);
  font: 700 17px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .08em;
}

/* Sponsored plates above a browse list: a strip that scrolls sideways, so
   they suggest without taking the page from the restaurants. */
.shop-strip {
  display: grid; grid-auto-flow: column; grid-auto-columns: 84%;
  gap: 12px; overflow-x: auto; padding: 2px 16px 10px; margin: 0 -16px;
  scroll-snap-type: x proximity; scrollbar-width: none;
}
.shop-strip::-webkit-scrollbar { display: none; }
.shop-strip .card { margin: 0; scroll-snap-align: start; }
@media (min-width: 560px) { .shop-strip { grid-auto-columns: 46%; } }

/* Live offers on the splash. Left-aligned inside a centred pitch, because
   a row of cards centred with the text reads as decoration. */
.splash-offers { width: 100%; margin: 30px 0 0; text-align: left; }
.splash-offers h2 {
  font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-dim); margin: 0 0 10px;
}

/* A photo in the Photos pile that already has a dish on it. Dimmed
   rather than hidden: the work left is what should stand out, but a named
   photo still has to be reachable to rename, sponsor or take down. */
.pile button.tagged img { opacity: .55; }
.pile button { position: relative; }
.pile-name {
  position: absolute; left: 4px; right: 4px; bottom: 4px;
  padding: 3px 6px; border-radius: 8px;
  background: color-mix(in srgb, var(--text) 72%, transparent);
  color: var(--bg); font-size: 11px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The header button once it carries a handle. Capped so a long name
   cannot push the masthead around, and quieter than an action button
   because it mostly just reports who you are. */
#authBtn.me {
  max-width: 40vw; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-weight: 600;
}

/* Add to Home Screen instructions.
   The share glyph is drawn rather than described, because the whole
   instruction is "tap the thing that looks like this". */
.shareglyph {
  display: inline-block; width: 13px; height: 16px; vertical-align: -3px;
  margin: 0 2px;
  background: currentColor; color: var(--accent);
  -webkit-mask: var(--share-mask) center/contain no-repeat;
  mask: var(--share-mask) center/contain no-repeat;
}
:root {
  --share-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12M8.5 6.5L12 3l3.5 3.5"/><path d="M7 11H5.5A1.5 1.5 0 0 0 4 12.5v6A1.5 1.5 0 0 0 5.5 20h13a1.5 1.5 0 0 0 1.5-1.5v-6A1.5 1.5 0 0 0 18.5 11H17"/></svg>');
}

#a2hsDlg .body { display: flex; flex-direction: column; }
.howto { margin-top: 4px; }
.howto h3 {
  font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-dim); margin: 14px 0 8px;
}
.howto ol { list-style: none; margin: 0; padding: 0; }
.howto li {
  padding: 12px 14px; border-radius: 12px; background: var(--surface-2);
  margin-bottom: 8px; font-size: 15px;
}
.howto li b { display: inline; }
.howto li span { display: block; margin-top: 4px; font-size: 14px; line-height: 1.5; color: var(--text-dim); }
.howto li span b { color: var(--text); }
.rec {
  display: inline-block; margin-left: 6px; padding: 2px 8px;
  border-radius: 999px; font-size: 11px; font-weight: 700;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent); text-transform: uppercase; letter-spacing: .06em;
}
#a2hsGo { width: 100%; padding: 14px; }

/* A plate a restaurant is sponsoring. It is the same card as any other —
   same likes, same menu, same long press — so the treatment is a border
   and nothing more. Anything louder would make it read as a different
   kind of object, which is exactly what it is not. */
.card.sponsored {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 18%, transparent), var(--shadow);
}

/* The offer on a sponsored card in the feed. */
.promo-offer {
  margin: 8px 0 0; font-weight: 600; color: var(--accent);
  letter-spacing: -.01em;
}

/* An anchor that looks like a button. Used where the action is really a
   navigation, so it gets the router's link handling and a real URL for
   middle-click and long-press. */
.btn-link {
  display: inline-block; text-decoration: none;
  font: 600 15px/1 ui-rounded, -apple-system, system-ui, sans-serif;
  border-radius: 12px; padding: 10px 18px;
  background: var(--surface-2); color: var(--text);
}
.btn-link:active { opacity: .8; }

/* The banner is a link now, not a button. */
a.here { text-decoration: none; }

/* Several restaurants within arm's reach: name them all, let the person
   choose. Guessing one and being wrong is worse than asking. */
.here-many { flex-direction: column; align-items: stretch; gap: 10px; }
.here-top { display: flex; align-items: center; gap: 10px; }
.here-top svg { width: 20px; height: 20px; flex: none; color: var(--accent); }
.here-picks { display: flex; flex-wrap: wrap; gap: 8px; padding-left: 30px; }
.here-pick {
  display: inline-block; text-decoration: none;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; padding: 7px 14px;
  font-size: 14px; font-weight: 600; color: var(--accent);
}
.here-pick:active { opacity: .75; }

/* Said in the picker when the app genuinely cannot tell two places
   apart, rather than pretending it can. */
.ambiguous {
  background: color-mix(in srgb, var(--star) 16%, var(--surface));
  border-radius: 10px; padding: 10px 12px; margin: 0 4px 10px;
}

/* Claiming, at the foot of the page where an owner will look for it and
   a diner will not trip over it. */
.claim-foot { text-align: center; padding: 26px 0 8px; margin: 0; }
.claim-foot a {
  font-size: 14px; color: var(--text-dim); text-decoration: none;
  border-bottom: 1px solid var(--line); padding-bottom: 2px;
}
.claim-foot a:active { color: var(--accent); }

/* An owner's own page, before they've paid for it. */
.activate {
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line));
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
  border-radius: var(--radius); padding: 16px; margin: 14px 0;
}
.activate h2 { margin: 0 0 6px; font-size: 17px; }
.activate p { margin: 0 0 12px; font-size: 15px; color: var(--text-dim); }

/* --- best near you ---------------------------------------------------
   Leaderboard rows. Deliberately not shaped like a post card: a ranking
   is a list you scan, not a plate you linger on. */

.sec {
  margin: 22px 0 10px; font-size: 15px; letter-spacing: .01em;
  color: var(--text-dim); font-weight: 700; text-transform: none;
}

/* 4:3 like everything else. At 16/9 this re-cropped an image that is already
 * 4:3 and clipped the badge off the bottom — the one surface still doing its
 * own cropping after the composer stopped needing it to. */
/* Shared by the invite card, a Browse row and the profile. The `.card.rank`
   family it was born with went with the Best screen on 2026-10-07; these three
   are all that still use it. */
.rank-main { min-width: 0; }
.rank-main b { display: block; line-height: 1.25; }
.rank-main small { display: block; color: var(--text-dim); font-size: 13px; margin-top: 2px; }

/* --- rewards ---------------------------------------------------------
   The diner's side of a sponsored photo. An expired or spent one is
   dimmed rather than hidden: a reward that silently vanishes reads as a
   cheat, one that says "used" reads as a record. */

.pill.done { background: var(--surface-2); color: var(--text-dim); }

.card.reward.redeemed, .card.reward.expired { opacity: .62; }
.card.reward .code {
  margin: 12px 0 0; font-size: 22px; letter-spacing: .18em;
  text-align: center; word-break: normal;
}

#sponsorShot {
  width: 100%; display: block; border-radius: 10px;
  aspect-ratio: 4/3; object-fit: cover; background: var(--surface-2);
}
.lbl { font-size: 13px; font-weight: 600; color: var(--text-dim); }

.codes { display: flex; flex-direction: column; gap: 6px; }
.code-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-radius: 10px; background: var(--surface);
  border: 1px solid var(--line);
}
.code-row span { flex: 1; min-width: 0; font-size: 14px; color: var(--text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.code-row small { flex: none; font-size: 12px; color: var(--text-dim); }
.code-row.done { opacity: .55; }

/* --- profiles: a resume for your taste in food -----------------------
   Composed, not generated. What is not here is not hidden — it was never
   switched on. */

.who {
  font-weight: 700; color: var(--text); text-decoration: none;
  display: inline-block;
}
.who:hover { text-decoration: underline; }

.pf-top { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
.pf-top h1 { margin: 0; font-size: 22px; line-height: 1.2; }
.pf-top .meta { margin: 2px 0 0; }
.pf-face {
  width: 58px; height: 58px; border-radius: 50%; object-fit: cover;
  background: var(--surface-2); flex: none;
}
.pf-face.blank { background: var(--surface-2); }
.pf-bio { margin: 12px 0 0; font-size: 15px; }

.card.slot .body { padding: 12px 14px; }
.slot-label {
  margin: 0 0 4px; font-size: 12px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent);
}

.stats {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
  margin-top: 4px;
}
.stats > div {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px 6px; text-align: center;
}
.stats b { display: block; font-size: 20px; line-height: 1.1; }
.stats small { display: block; font-size: 11px; color: var(--text-dim); margin-top: 3px; }

.slot-edit {
  border: 1px solid var(--line); border-radius: 12px;
  padding: 10px 12px; margin-bottom: 8px;
}
.slot-edit + .slot-edit { margin-top: 0; }
.slot-edit input { margin-top: 8px; }

.dish-pick {
  border: 1px solid var(--accent); border-radius: 12px; padding: 10px 12px;
  margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 6px;
}
.dish-pick .lbl { width: 100%; margin: 0 0 4px; }

.toggle {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 2px; font-size: 15px; cursor: pointer;
}
.toggle input { width: 18px; height: 18px; flex: none; accent-color: var(--accent); }

a.code-row { text-decoration: none; color: inherit; }
a.code-row:hover { border-color: var(--accent); }

/* --- collections and sends ------------------------------------------- */

button.code-row, .col-row {
  width: 100%; text-align: left; background: var(--surface);
  color: inherit; font: inherit; cursor: pointer;
}
.col-row:disabled { opacity: .5; }
.code-row small { color: var(--text-dim); }

.send {
  border: 1px solid var(--line); border-radius: 14px;
  padding: 12px; margin-top: 12px; background: var(--surface);
}
.send.unread { border-color: var(--accent); }
.send-from { margin: 0 0 10px; font-size: 14px; color: var(--text-dim); }
.send-from b { color: var(--text); cursor: pointer; }
.send .card { margin: 0 0 10px; }

/* The `hidden` attribute is only `display: none` in the user-agent
   stylesheet, so ANY author rule that sets `display` outranks it — an
   element can be `hidden` and still painted. That is how the old post
   button kept sitting on top of the tab bar after being hidden. This is
   the rule everyone writes eventually; better early than after the next
   one. */
[hidden] { display: none !important; }

/* --- bottom tab bar ---------------------------------------------------
   Post sits in the middle, where a thumb already is. The old floating
   button is gone: two ways to do the same thing is one too many. */

.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  display: grid; grid-template-columns: repeat(5, 1fr);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.tb {
  background: none; border: 0; padding: 9px 0 7px; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  color: var(--text-dim); font: 600 10px/1 inherit; position: relative;
}
.tb svg { width: 23px; height: 23px; }
.tb.on { color: var(--accent); }
/* OFF-WHITE, LIKE THE REST OF THE CONTROLS. Robin's call, 2026-10-07. It was
   an accent disc with a white plus on it — the one remaining burgundy fill in
   the furniture, sitting in the middle of the bar on every screen.

   The plus goes dark with it, because a white stroke on `--surface-2` is
   invisible. And it gains the hairline every other off-white control has: the
   bar itself is `--surface` at 92%, so an off-white disc on it has almost
   nothing to read against without one.

   Still a disc, not a squared pill. Like `button.icon-btn` this is a target
   with nothing written in it, and the squaring pass left those round.

   The drop shadow was `rgba(168,48,12,.34)` — an orange, left from a palette
   before this one; the accent has been `#8a1f2b` since. Neutral now, and much
   lighter, because a heavy shadow under a pale disc reads as a smudge. */
.tb-post {
  color: var(--text);
}
.tb-post svg {
  width: 40px; height: 40px; padding: 8px; border-radius: 50%;
  background: var(--surface-2); color: var(--text);
  box-shadow: inset 0 0 0 1px var(--line), 0 2px 8px rgba(0, 0, 0, .08);
}
/* THE MARK THAT SOMETHING HAS ARRIVED. It sat on the You tab until 2026-10-07
   and now sits on the header's bell, where the thing it is about is one tap
   away. `.icon-btn` gains the containing block it needs; without that the dot
   would resolve against the page, which is the same mistake the press hint made
   on the profile reel. */
button.icon-btn { position: relative; }
.icon-btn .dot {
  position: absolute; top: 3px; right: 3px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
  border: 2px solid var(--bg);
}
/* The fixed bar covers the foot of the page, so everything in the flow
   needs clearance — not just <main>. Putting it there was the bug that
   buried the "What is Saucer?" link, which lives after main and so got
   none of it. On the body it covers whatever ends up last. */
body { padding-bottom: calc(76px + env(safe-area-inset-bottom)); }

.badge {
  margin-left: auto; background: var(--accent); color: var(--accent-ink);
  font: 700 11px/1 inherit; padding: 3px 7px; border-radius: 999px;
}

/* --- filter rows ------------------------------------------------------ */

.filters { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 6px; }
/* The restaurant picker takes its own row: names are long, and the two
   controls above it are a word each. */
.filters select.wide { flex: 0 0 100%; }

/* The name finder. Sits above the area filters on Browse, and is the only
   control on the screens that have no position to work from. */
.finder { margin: 14px 0 4px; }
.finder input {
  width: 100%; box-sizing: border-box; padding: 11px 12px;
  border-radius: 10px; border: 1px solid var(--line);
  background: var(--surface); color: var(--text); font: inherit; font-size: 15px;
}
.finder-list { margin-top: 8px; display: grid; gap: 8px; }
.finder-list .card.place { width: 100%; text-align: left; }

/* The address on a restaurant page, which opens directions. Underlined
   rather than coloured: it sits inside a line of grey meta text, and a
   coloured word there reads as an error. */
.where-go { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* "Long press for actions", shown when somebody taps a photo.
   Positioned against the card, which is why the card is a containing block. */
.card, .card.place, .card.promo { position: relative; }
.press-hint {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  background: rgba(0, 0, 0, .78); color: #fff;
  padding: 7px 13px; border-radius: 999px;
  font-size: 13px; font-weight: 600; white-space: nowrap;
  pointer-events: none; z-index: 3;
  animation: hintIn .16s ease-out, hintOut .3s ease-in 1.3s both;
}
/* The same hint on a photo-grid tile, which is a third of a phone wide and
   clips its overflow. Wraps instead of being cut in half. */
.press-hint.tight {
  font-size: 11px; padding: 5px 9px; bottom: 8px;
  white-space: normal; text-align: center; line-height: 1.25;
  max-width: calc(100% - 12px);
}
@keyframes hintIn { from { opacity: 0; transform: translate(-50%, 6px); } }
@keyframes hintOut { to { opacity: 0; } }

/* A dish with no photographs, shown to its owner only so they can delete
   it. Dimmed rather than coloured: it is not an error, it is a leftover. */
.dish.empty { opacity: .62; }
.dish.empty .d-meta { font-style: italic; }

/* Over a plate of your own that is flagged or has been removed. Nobody else
   is ever served these plates, so this is only ever seen by its author. */
.veil {
  position: absolute; left: 0; right: 0; top: 0;
  display: grid; gap: 3px; padding: 12px 14px;
  background: linear-gradient(180deg, rgba(0,0,0,.78), rgba(0,0,0,.45) 78%, transparent);
  color: #fff; z-index: 2; pointer-events: none;
}
.veil b { font-size: 14px; }
.veil span { font-size: 13px; line-height: 1.35; opacity: .92; }
.veil.gone { background: linear-gradient(180deg, rgba(120,20,10,.9), rgba(120,20,10,.5) 78%, transparent); }
/* The moderator's mark. Deliberately NOT the red of `.gone`: a restaurant
   taking a photograph off its own page is a choice the product gives it, and
   colouring that as a violation would misread most of what lands here. */
.veil.hush { background: linear-gradient(180deg, rgba(10,22,40,.88), rgba(10,22,40,.5) 78%, transparent); }
/* Dimmed so it reads as withdrawn rather than live. */
.card:has(.veil) .plate { filter: saturate(.5) brightness(.8); }





.filters select {
  flex: 1; min-width: 0; padding: 9px 10px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--text); font: inherit; font-size: 14px;
}

.at { color: var(--text-dim); font-size: 18px; padding: 0 2px; }

/* Destructive rows. Red enough to be read as a warning, not so red that
   the sheet looks like an error. */
.sheet-item.danger { color: var(--bad, #c0392b); }
.sheet-item.danger svg { stroke: var(--bad, #c0392b); }
.ghost.danger { color: var(--bad, #c0392b); border-color: var(--bad, #c0392b); }



/* --- per-restaurant reels on a profile -------------------------------- */

.reel { margin-top: 16px; }
.reel-head {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px;
}
.reel-head b { font-size: 15px; }
.reel-head .muted { font-size: 13px; }
.reel-strip {
  display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch; padding-bottom: 4px;
}
.reel-strip::-webkit-scrollbar { display: none; }
.reel-shot {
  /* THE CONTAINING BLOCK FOR THE PRESS HINT. Without it the absolutely
     positioned pill resolved against the page, so tapping a plate on somebody's
     profile put "Long press for actions" at the bottom of the screen instead of
     on the photograph. `.card` and `.tile` both set this; the reel did not. */
  position: relative;
  flex: 0 0 72%; max-width: 280px; margin: 0; scroll-snap-align: start;
  border-radius: 14px; overflow: hidden; background: var(--surface);
  border: 1px solid var(--line);
}
.reel-shot img {
  width: 100%; display: block; aspect-ratio: 4/3; object-fit: cover;
  background: var(--surface-2);
}
.reel-noshot { width: 100%; aspect-ratio: 4/3; background: var(--surface-2); }
.reel-shot figcaption {
  padding: 9px 11px; font-size: 13px; color: var(--text-dim);
  display: flex; align-items: center; gap: 8px; min-height: 20px;
}
.reel-shot figcaption span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --- browse cards -----------------------------------------------------
   These were <a> elements and inherited the browser's link colours, so a
   list of restaurants read like a 1998 index page. They are cards now,
   and any anchor rendered as a card takes the card's colour rather than
   the user agent's. */

a.card, a.code-row, .card a.first-go {
  color: inherit; text-decoration: none;
}

.card.place { cursor: pointer; }
.card.place .body { padding: 13px 14px; }
.card.place:active { transform: scale(.995); }
.card.place.pressing { background: var(--surface-2); }

/* The empty state, which for a while is nearly every listing. An
   invitation, not an apology. */
.first {
  margin-top: 11px; padding: 11px 12px; border-radius: 11px;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border: 1px dashed color-mix(in srgb, var(--accent) 34%, transparent);
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
}
.first b { color: var(--accent); font-size: 15px; }
.first span { flex: 1 1 100%; font-size: 13px; color: var(--text-dim); }
.first-go {
  flex: none; padding: 6px 12px; border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface);
  font-size: 13px; font-weight: 600;
}

.splash-why {
  margin: 12px auto 0; max-width: 34ch; font-size: 13px;
  color: var(--text-dim); text-align: center; line-height: 1.5;
}

/* An invitation in the feed. Reads as a card like any other, with the
   invitation itself doing the talking rather than a different shape. */

/* --- no location -------------------------------------------------------
   Not an error state. Saucer without a location has nothing honest to
   show, so it says what it is instead and offers the one thing that
   changes that. */

.nogeo { text-align: center; padding: 26px 4px 8px; }
.nogeo-mark { display: block; margin: 0 auto 14px; }
.nogeo h2 { margin: 0 0 10px; font-size: 21px; line-height: 1.2; }
.nogeo > p {
  margin: 0 auto 18px; max-width: 38ch; font-size: 15px;
  color: var(--text-dim); line-height: 1.55;
}
.nogeo .beats { text-align: left; margin: 0 auto 20px; max-width: 34ch; }
.nogeo #nogeoAct { margin-bottom: 16px; }
.nogeo #nogeoAct button { width: 100%; max-width: 20rem; }
.nogeo .note { max-width: 34ch; margin: 0 auto; }

/* --- add to home screen ------------------------------------------------
   An offer, not a nag: one line, dismissible, and gone for good once
   answered either way. */

.install {
  display: flex; align-items: center; gap: 11px;
  margin: 14px 0 4px; padding: 11px 12px;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface);
}
.install img { border-radius: 8px; flex: none; }
/* The moderation strip. Same shape as the install invitation, in a colour
   that reads as "something is waiting" rather than "something is broken" —
   an auto-hidden plate is a question, not a failure. */
.alert-strip {
  display: block; text-align: left; width: 100%;
  border-color: var(--warn, #c98a00);
}
.alert-strip b { display: block; font-size: 14px; margin-bottom: 2px; }
.alert-strip span { display: block; font-size: 13px; color: var(--text-dim); }

.install-say { min-width: 0; flex: 1; }
.install-say b { display: block; font-size: 14px; line-height: 1.25; }
.install-say span { display: block; font-size: 13px; color: var(--text-dim); margin-top: 2px; }
.install button.small { flex: none; }
.install-no {
  flex: none; background: none; border: 0; cursor: pointer;
  color: var(--text-dim); font-size: 20px; line-height: 1;
  padding: 2px 4px; margin-left: -2px;
}

/* --- long press belongs to us -----------------------------------------
   Long-pressing a photo on a phone raises the browser's OWN callout —
   Save Image, Copy, Share — on top of our sheet, so two menus arrive at
   once and neither is usable. preventDefault on `contextmenu` does not
   stop it: on iOS the image callout is not a contextmenu event, and only
   this CSS suppresses it.

   Selection is off on pressable cards too, so a long press on a caption
   opens the menu like a long press on the photo, instead of starting a
   text selection. The Share action in the sheet is the way to pass a
   plate on. */

#main img,
/* ON THE CARD'S IMAGE, not on every card. Every selector here describes a
   POSITION — inside #main, a card carrying data-post — so a card rendered
   anywhere else, or read before its dataset is set, got the phone's own Save
   Image sheet instead of Saucer's menu. `.card img` travels with the thing
   and fixes that.
   
   A BARE `.card` DID NOT. It was here for a day and turned text selection
   off in every card in the product, on every platform including desktop: a
   restaurant's address, a caption, a recipe line, a comment, the body text
   on Terms and Privacy. Unselectable text is a real loss — people copy an
   address — and none of it was ever the thing raising the callout. Only
   pressable images need this. */
.card img,
.card.place,
.card[data-post],
/* The splash's sponsored strip lives OUTSIDE #main, so none of the
   selectors above reached it: a long press on a sponsored photo on the
   splash raised Apple's own Save Image sheet on top of Saucer's. Browse's
   identical strip was covered only because it happens to be inside #main. */
.card.promo,
#splashOffersStrip img,
.reel-shot,
.pile button {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

#main img,
#splashOffersStrip img,
.reel-shot img,
.pile img {
  -webkit-user-drag: none;
  -khtml-user-drag: none;
}

/* --- the menu ---------------------------------------------------------
   IT HAS TO READ AS A MENU, not as a list of records. Three things were
   making it the latter: a photo count under every dish, which is the
   restaurant's bookkeeping and nobody else's business; no description, so a
   name and a number told you what to pay but not what you were eating; and
   prices that wandered, because every row was its own grid and sized its own
   columns. A leader fixes the third and is what a printed menu has always
   done. */
.sect-head {
  margin: 30px 0 10px; font-size: 19px; font-weight: 700;
  letter-spacing: -.01em; color: var(--text); text-align: center;
}
.sect-head::after {
  content: ""; display: block; width: 34px; height: 2px;
  background: var(--accent); margin: 8px auto 0; border-radius: 2px;
}
.m-row { padding: 11px 0; }
/* The leader. `flex: 1` on the dots is what pulls every price to the same
   right edge no matter how long the name is. */
.m-line { display: flex; align-items: baseline; gap: 8px; }
.m-dots {
  flex: 1; border-bottom: 1px dotted var(--line);
  transform: translateY(-4px); min-width: 14px;
}
.m-name {
  background: none; color: var(--text); padding: 0; text-align: left;
  font-size: 17px; font-weight: 600; border-radius: 0; flex: none;
}
.m-name:active { color: var(--accent); }
.m-price { font-size: 16px; font-weight: 600; color: var(--text); flex: none; }
/* An owner sees prices they have set even while unpublished, so the field is
   not a black box — dimmed and struck to say "not shown to guests". */
.m-price.off { color: var(--text-dim); text-decoration: line-through; }
/* The line that makes it a menu. */
.m-desc {
  margin: 3px 0 0; font-size: 14px; line-height: 1.45;
  color: var(--text-dim); max-width: 46ch;
}
/* Owner-only. A guest never sees how many photographs a dish has. */
.m-meta { font-size: 12px; color: var(--text-dim); margin-top: 3px; opacity: .8; }
.m-row.empty .m-name { color: var(--text-dim); }
.m-edit {
  background: var(--surface-2); color: var(--text-dim);
  padding: 5px 11px; font-size: 13px; margin-top: 6px;
}
/* Arrived here from a plate. Loud enough to find on a long menu, gone before
   it becomes decoration. */
.m-found { animation: foundit 2.4s ease-out; border-radius: 10px; }
@keyframes foundit {
  0%, 60% { background: color-mix(in srgb, var(--accent) 18%, transparent); }
  100% { background: transparent; }
}
.menu-admin {
  background: var(--surface-2); border-radius: var(--radius);
  padding: 12px 14px; margin: 14px 0; display: grid; gap: 8px;
}
.menu-admin .toggle { display: flex; align-items: center; gap: 9px; font-size: 15px; }
.menu-admin .note { margin: 2px 0 0; }

/* A code being typed by somebody else, on somebody else's phone, in a hurry.
   Big, spaced, and unmistakably not a normal text field. */
.bigcode {
  font: 700 26px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .32em; text-align: center; text-transform: uppercase;
  padding: 16px 14px;
}

/* Four numbers that have to be readable at a glance on a phone, by somebody
   deciding whether this is worth $100 again next year. */
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.stat-row > div { text-align: center; }
.stat-row b { display: block; font-size: 21px; line-height: 1.1; }
.stat-row small { font-size: 11px; color: var(--text-dim); }

/* --- the restaurant's own side ---------------------------------------
   It has to read as somewhere else within two seconds of landing, or the
   switch means nothing. A different masthead does most of that work. */
.owner-head {
  background: var(--surface-2); border-bottom: 1px solid var(--line);
  padding: 16px; margin: 0 -16px 4px;
}
.owner-head h1 { margin: 0; }
.owner-kicker {
  margin: 0 0 2px; font-size: 11px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--accent);
}
.owner-head select { margin-top: 10px; }
/* Said once at the top of the console rather than on every control: who you
   are here as, and whether the page is paid for. Carries the accent rule down
   its edge so it reads as the screen talking about itself, not as content. */
.owner-note {
  margin: 10px 0 0; padding: 9px 12px; font-size: 13px; line-height: 1.4;
  color: var(--text-dim); background: var(--surface-2);
  border-left: 3px solid var(--accent); border-radius: 0 10px 10px 0;
}

/* A wall of photographs the owner works through. Square, dense, and the
   like count on the tile because that is what they are hunting for. */
.pile { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-top: 12px; }
.tile { padding: 0; border-radius: 8px; overflow: hidden; position: relative; background: none; }
.tile img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
/* Already on a dish. Dimmed rather than hidden: the owner still needs to
   reach it, to move it or to swap it onto something else. */
.tile.filed img { opacity: .45; }
.tile-likes {
  position: absolute; right: 5px; bottom: 5px;
  background: rgba(0,0,0,.55); color: #fff;
  font-size: 11px; font-weight: 700; padding: 1px 6px; border-radius: 999px;
}

/* The admin directory. Chips rather than a list: it is scanned, not read, and
   a thumb has to hit one of them. */
.dir { display: flex; flex-wrap: wrap; gap: 7px; }
.dir a {
  background: var(--surface-2); color: var(--text); text-decoration: none;
  padding: 7px 13px; border-radius: 10px; font-size: 14px; font-weight: 600;
}
.dir a:active { background: var(--accent); color: var(--accent-ink); }
.dir-head {
  margin: 10px 0 0; font-size: 11px; font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase; color: var(--text-dim);
}

/* "Add a dish", under the menu's own settings. Sits with the card rather than
   floating over the list: building a menu is a deliberate sit-down job, not
   something done with a thumb while scrolling. */
.menu-admin + button { margin: 10px 14px 0; }

/* A dropdown in a dialog is a form field like the inputs around it.
   The base rule styles `input, textarea` and never mentioned `select`, so a
   dropdown stayed inline at its OS default width — and the label after it
   flowed up alongside, which put "Description" on the same line as the Section
   dropdown in the dish dialog. Scoped to dialogs on purpose: the filter bar
   and the owner's restaurant picker size their selects deliberately. */
dialog select {
  font: inherit; width: 100%; padding: 13px 14px; color: var(--text);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); outline: none;
}
dialog select:focus { border-color: var(--accent); }

/* The optional photo strip on "Add a dish". Scrolls sideways rather than
   wrapping: a restaurant with eighty unassigned photographs should not get a
   dialog eighty rows tall, and sideways keeps the Save button on screen.

   NOT `.dish-pick` — that name was already taken, by the accented box the
   assign sheet draws over a plate (app.js:6788). Reusing it gave both
   components half of each other's rules: this one inherited a border and a
   wrap it does not want, and that one lost its wrap to a sideways scroll. */
.dish-strip {
  display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 6px;
  -webkit-overflow-scrolling: touch;
}
.dish-strip button {
  flex: 0 0 76px; width: 76px; height: 76px; padding: 0; border-radius: 10px;
  overflow: hidden; background: var(--surface-2); border: 2px solid transparent;
}
.dish-strip img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The chosen one says so without a tick covering the food. */
.dish-strip button.on { border-color: var(--accent); }
.dish-strip button.on img { opacity: .72; }


/* --- the recipe builder ----------------------------------------------
 *
 * A row is a line of the recipe, and every row carries its own minus. The
 * whole screen is one column of rows with one Add at the bottom, because
 * that is the shape of the thing being made: a list you grow a line at a
 * time. Nothing here is a grid — a grid cannot have rows of different
 * kinds in one order.
 */
.rb-row {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 7px 0; border-bottom: 1px solid var(--line);
}
.rb-row:last-of-type { border-bottom: 0; }
/* The body of a row grows; the minus never does. */
.rb-main { flex: 1; min-width: 0; display: flex; gap: 6px; align-items: flex-start; }
.rb-main input, .rb-main textarea, .rb-main select {
  padding: 9px 10px; border-radius: 10px;
}
/* Wide enough for "12 1/2" and no wider — on a phone every pixel this takes
   is a pixel the ingredient name does not get. */
/* Wide enough for a fraction glyph and the dropdown arrow beside it. */
.rb-amt { width: 78px; flex: none; }
.rb-unit { width: 104px; flex: none; }
.rb-item { flex: 1; min-width: 0; }
.rb-text { width: 100%; min-height: 52px; }

/* The minus. Square, quiet, and the same size as a thumb — it sits beside
   every row and must be hittable without being the thing you see first. */
.rb-minus {
  flex: none; width: 34px; height: 34px; padding: 0; margin-top: 2px;
  border-radius: 10px; background: var(--surface-2); color: var(--text-dim);
  font-size: 20px; line-height: 1; font-weight: 600;
}
.rb-minus:active { background: #d1442a; color: #fff; }

/* A step shows its number, which is the one thing a numbered list has to
   do and the one thing you cannot type yourself without it going wrong the
   moment a step is inserted or removed. */
.rb-n {
  flex: none; width: 24px; padding-top: 12px; text-align: right;
  font-size: 13px; font-weight: 700; color: var(--text-dim);
}
/* `rb-section`, not `rb-sect`. The block type is "section" and drawRows emits
   `rb-row rb-${b.type}`, so this rule matched nothing and a recipe's section
   headings were never bold. */
.rb-section input { font-weight: 700; }
.rb-note textarea { font-style: italic; }
.rb-kind {
  font-size: 11px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--text-dim);
  padding-top: 14px; width: 24px; flex: none;
}

/* Add sits under the rows, full width, and is the only way to grow the
   recipe — so it is the one thing on the screen that looks like a button. */
.rb-add { width: 100%; margin: 14px 0 4px; }
.rb-meta { display: flex; gap: 8px; }
.rb-meta label { flex: 1; }
.rb-empty { color: var(--text-dim); padding: 18px 2px; }

/* --- cooking ----------------------------------------------------------
 *
 * One step, as large as the screen allows, for somebody holding a phone
 * with one hand and a pan in the other. Everything else is removed.
 */
.cook-wrap { display: flex; flex-direction: column; min-height: 72vh; }
.cook-where { font-size: 13px; font-weight: 700; color: var(--text-dim);
  letter-spacing: .04em; text-transform: uppercase; }
/* WHY min-width AND overflow-wrap, and why the page scrolled sideways
   without them.
 *
 * `.cook-step` is a flex container and `.cook-inner` is its item. A flex item
 * defaults to `min-width: auto`, which means it refuses to shrink below the
 * intrinsic width of its content — so one long unbreakable token in a step
 * (a brand name, a measurement, a pasted URL) set at 25px made the item wider
 * than the phone, and the whole document scrolled sideways rather than the
 * word wrapping. `width: 100%` does not help: it sets the preferred width and
 * the minimum still wins.
 *
 * So the item is allowed to shrink, and the text is allowed to break. The
 * break is `anywhere` rather than `break-word` because at 25px on a 360px
 * screen there are real words that cannot fit on a line at all, and a word
 * split across two lines is better than a step you have to scroll to read
 * while holding a pan. */
.cook-step { flex: 1; display: flex; align-items: center; min-width: 0; }
.cook-inner { width: 100%; min-width: 0; }
.cook-step p {
  font-size: 25px; line-height: 1.35; font-weight: 500; margin: 0;
  overflow-wrap: anywhere;
  /* Steps are written with line breaks in them and they are meaningful. */
  white-space: pre-wrap;
}
.cook-ings li { overflow-wrap: anywhere; }
.cook-sect { font-size: 15px; color: var(--accent); font-weight: 700; margin: 0 0 10px; }
.cook-nav { display: flex; gap: 10px; }
.cook-nav button { flex: 1; padding: 16px; }
.cook-ings { margin: 0; padding: 0; list-style: none; }
.cook-ings li { padding: 7px 0; border-bottom: 1px solid var(--line); }

/* The menu-image honour. Warmer than the stat grid above it because it is
   the one number somebody was GIVEN rather than one they accumulated. */
.honor-line {
  margin: 10px 0 0; padding: 12px 14px; border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line));
  font-size: 14px;
}
.honor-line b { color: var(--accent); }

/* --- a restaurant chose your photograph -------------------------------
 *
 * The one piece of news in Saucer that is good news about YOU, so it is
 * the one card allowed to look pleased with itself. Accent border and a
 * flag along the top; everything else in the inbox stays quiet so this
 * does not have to shout to be the thing you see first.
 */
.honor-card {
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
  border-radius: var(--radius); padding: 0 0 12px; overflow: hidden;
}
.honor-flag {
  margin: 0; padding: 9px 14px;
  background: var(--accent); color: var(--accent-ink);
  font-size: 12px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase;
}
.honor-card .plate { width: 100%; display: block; }
.honor-said { margin: 12px 14px; font-size: 16px; line-height: 1.4; }
.honor-said b { color: var(--accent); }
.honor-card > button { margin: 0 14px 0; }

/* The code, in the shape it is read in: big, monospaced, and spaced out
   so it can be read aloud across a counter without being repeated. */
.honor-code {
  margin: 12px 14px; padding: 12px 14px; border-radius: 12px;
  background: var(--surface); border: 1px dashed var(--line);
  text-align: center;
}
/* NOT a code — the guest never has one. This is the fact that a discount
   exists, said once and plainly, with the mechanic underneath it. */
.honor-code b {
  display: block; margin: 6px 0;
  font-size: 17px; font-weight: 700; color: var(--accent);
}
.honor-code small { display: block; font-size: 12px; color: var(--text-dim); }

/* THE A–Z ROLODEX on the admin section. A wrapping row of small keys rather
   than a scroller: twenty-seven targets have to be reachable without a gesture,
   and on a phone that means two or three lines. */
.azrow { display: flex; flex-wrap: wrap; gap: 5px; }
.azk {
  background: var(--surface-2); color: var(--text-dim);
  padding: 6px 0; font-size: 13px; font-weight: 600;
  min-width: 30px; text-align: center; border-radius: 8px;
}
.azk.on { background: var(--accent); color: var(--accent-ink); }

/* THE FEED WHILE THE NEXT SET LOADS.
   `load()` used to blank `main` to "Loading…" on every call, and every filter
   change is a call — so changing the window threw away a painted screen and a
   fast answer still read as a full reload. The cards stay where they are,
   dimmed and inert, and are replaced in one go when the new ones arrive.
   `pointer-events` matters as much as the opacity: a card that is on its way
   out must not take a tap and open a plate the next paint removes. */
#main.reloading {
  opacity: .5;
  pointer-events: none;
  transition: opacity .12s ease-out;
}

/* PLATES / RESTAURANTS — the Feed tab's two questions.
   Browse moved in here behind this control on 2026-10-07 so its tab could be
   freed for Menus. Sized and spaced like the filter row it sits above, because
   it IS a filter in the way that matters: it decides what the list is made of.
   The active side is not a button — it has no handler and no pointer — so
   there is never a tap that appears to do nothing. */
.viewtog {
  display: flex;
  gap: 12px;
  margin: 16px 0 4px;
}

/* BUTTONS, NOT PILLS, AND NOT A SLAB. Robin, 2026-10-07: too big, too close
   together, and he does not want the pill shape.
 *
 * The size problem was not really the padding — it was that the chosen side
 * was half the screen filled solid with `--accent`, which read as a banner
 * rather than a control and dwarfed the four pills directly above it. So the
 * fill is gone: the chase ring marks which one is live, exactly as it does on
 * the pills, and the label takes the accent colour. One indicator, not two.
 *
 * A 12px radius instead of 999px. These are a pair of equal halves rather
 * than a row of lozenges, and a rectangle reads as a segmented control where
 * a capsule reads as a tag. */
.viewtog .vt {
  position: relative;
  isolation: isolate;
  flex: 1;
  padding: 12px 14px;
  font-size: 14px;
  font-weight: 600;
  border: 0;
  border-radius: 12px;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line);
  color: var(--text-dim);
  cursor: pointer;
  transition: color .15s ease, background .15s ease, box-shadow .15s ease;
}
.viewtog .vt:not(.on):hover {
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--text-dim);
}
.viewtog .vt.on {
  background: var(--surface-2);
  /* The LABEL goes bright, not accent-coloured. `--accent` on `--surface-2`
     measures 3.87:1, short of AA for 14px bold, and the ring is already
     doing the colour work — two accent cues on one control is one too many
     anyway. Bright label plus a lit edge reads as "this one" at a glance. */
  color: var(--text);
  box-shadow: none;
  cursor: default;
}

/* THE ADMIN HUB. Robin, 2026-10-07: every admin job on its own page, reached
   from one screen, because a single column with every list stacked on it reads
   fine with four restaurants and is unusable with four thousand. */

/* The review queue, full width and above the grid, because it is the only one
   of these that is a backlog rather than a destination — it has a number and
   the number goes up on its own. */
.adbig {
  display: block; margin: 0 0 12px; padding: 15px 16px;
  border: 1px solid var(--accent); border-radius: 14px;
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  color: var(--text); text-decoration: none;
}
.adbig b { display: block; font-size: 17px; }
.adbig small { display: block; margin-top: 3px; color: var(--text-dim); }

.adgrid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 22px;
}
/* One column on a narrow phone: two 14px titles side by side wrap to three
   lines each and the grid stops being scannable, which is its only job. */
@media (max-width: 400px) { .adgrid { grid-template-columns: 1fr; } }

.adtile {
  display: block; padding: 13px 14px;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface); color: var(--text); text-decoration: none;
}
.adtile b { display: block; font-size: 14px; }
.adtile small { display: block; margin-top: 2px; color: var(--text-dim); font-size: 12px; }

.adh2 { margin: 0 0 10px; font-size: 15px; }

/* The filter above an admin list. It narrows the rows already on screen and
   says so in its own placeholder — searching a whole collection properly needs
   per-collection indexes and is written down in docs/BACKLOG.md rather than
   implied by a box that looks like it does more than it does. */
.adfilter {
  width: 100%; margin: 0 0 10px; padding: 9px 11px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: var(--text); font-size: 14px;
}

/* Your own plate's like count, with no button under it. A span rather than a
   disabled button: a greyed-out control invites a tap and then refuses it,
   which is a worse answer than not offering one. Matches `.like`'s metrics so
   the row does not shift between your plates and everyone else's. */
.act .like.self {
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--text-dim); cursor: default; background: none; border: 0;
}
.act .like.self svg { opacity: .45; }

/* ── /get, the QR landing ──────────────────────────────────────────────────
   Modelled on the splash, because it is the same kind of screen: one thing
   to do, nothing else on it. The steps are the content here though, so they
   get the room the splash gives its headline. */
#getApp { padding-block: 8px 48px; }

.get-in {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.get-mark {
  width: 104px; height: 104px; display: block;
  margin: 14px 0 16px;
  border-radius: 22px;
}

.get-h {
  font-size: clamp(26px, 7.5vw, 34px);
  line-height: 1.14;
  letter-spacing: -.025em;
  margin: 0 0 10px;
  text-wrap: balance;
}

.get-sub {
  margin: 0 0 24px;
  font-size: 16px;
  line-height: 1.5;
  color: var(--text-dim);
  max-width: 34ch;
  text-wrap: pretty;
}

#getPrompt { width: 100%; max-width: 320px; margin: 0 0 8px; }
#getPrompt button { width: 100%; font-size: 17px; padding-block: 14px; }

/* LEFT-ALIGNED, ALONE ON THE SCREEN. Centred step text is slower to follow:
   every line starts somewhere different and the eye has to find it again. */
#getSteps {
  width: 100%; max-width: 340px;
  text-align: left;
}

.get-lead {
  margin: 0 0 16px;
  font-size: 15px; line-height: 1.5;
  color: var(--text-dim);
}

.get-steph {
  margin: 0 0 14px;
  font-size: 15px;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.get-ol {
  margin: 0; padding: 0;
  list-style: none;
  counter-reset: step;
}

/* The number is a token rather than a bullet, so a step that wraps to three
   lines still reads as one step. */
.get-ol li {
  counter-increment: step;
  position: relative;
  padding: 0 0 18px 42px;
  font-size: 16px;
  line-height: 1.45;
}

.get-ol li::before {
  content: counter(step);
  position: absolute;
  left: 0; top: -1px;
  width: 28px; height: 28px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* The run of steps, drawn down the left so three steps read as one sequence
   rather than three sentences. */
.get-ol li:not(:last-child)::after {
  content: "";
  position: absolute;
  left: 13px; top: 30px; bottom: 6px;
  width: 2px;
  background: var(--line);
}

.get-ol li b { font-weight: 600; }

.get-then {
  width: 100%; max-width: 340px;
  margin: 10px 0 0;
  text-align: left;
  font-size: 15px; line-height: 1.5;
  color: var(--text-dim);
  padding-top: 18px;
  border-top: 1px solid var(--line);
}

.get-else {
  width: 100%; max-width: 340px;
  margin-top: 26px;
  text-align: left;
}

.get-else summary {
  font-size: 15px;
  color: var(--text-dim);
  cursor: pointer;
  padding: 10px 0;
}

.get-else h3 {
  margin: 20px 0 10px;
  font-size: 14px;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Quieter than the detected set — these are the ones we did not pick. */
.get-else .get-ol li {
  padding-bottom: 12px;
  font-size: 15px;
}
.get-else .get-ol li::before {
  width: 22px; height: 22px; font-size: 13px;
  background: var(--surface-2);
  color: var(--text);
}
.get-else .get-ol li:not(:last-child)::after { left: 10px; top: 24px; }

/* --- the two actions on Your recipes ------------------------------------
   Write a recipe and Search all recipes, which were a default button and a
   .ghost stacked down the page — two 999px lozenges on a screen whose every
   neighbour is a squared pill with the chase ring on it. Robin's call,
   2026-10-07: make them look like the rest of the site.

   The shape is `.tab`'s, not a new one: same 12px radius, same weight, same
   hairline, so the recipes screen is not inventing a third kind of button.

   No chase ring. That marks the tab you are on, and these are actions. */
.recacts {
  display: flex;
  gap: 10px;
  margin-top: 18px;
}

.recact {
  position: relative;
  isolation: isolate;
  /* Equal shares of a plate's width, like the feed tabs — so a two-word label
     and a three-word one still come out the same size. */
  flex: 1;
  min-width: 0;
  background: var(--surface-2);
  color: var(--text);
  padding: 13px 10px;
  font-size: 14px;
  font-weight: 600;
  border-radius: 12px;
  box-shadow: inset 0 0 0 1px var(--line);
  transition: color .15s ease, box-shadow .15s ease;
}

.recact:hover { box-shadow: inset 0 0 0 1px var(--text-dim); }

/* --- a recipe's actions, with @kitchen ----------------------------------
   Cook this, Remove, and the plus, as one centred row. Robin, 2026-10-08.

   The plus was on its own line above them, which wasted a band of the screen
   and — the real problem — made a 52px disc floating alone, which is exactly
   what the tab bar's post button is. Two identical discs on one screen doing
   different things. Inline at button height it is the third control in a
   group, which is what it is. */
.recrow {
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

/* SIZED TO ITS NEIGHBOURS, not to the nav. Round, because it is still the add
   gesture, but at the height of the buttons beside it so the row has one
   baseline — and so it stops reading as a second post button. */
.rec-add {
  flex: none;
  width: 44px; height: 44px; padding: 0;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--line);
  display: grid; place-items: center;
}
.rec-add svg { width: 22px; height: 22px; display: block; }

/* Turning while it works. An upload plus a Cloud Vision check is not instant,
   and without this a tap that takes three seconds looks like a tap that did
   nothing — the same reasoning as `.icon-btn.spinning`. */
.rec-add.working svg { animation: spin .7s linear infinite; }

/* --- @kitchen on a profile ---------------------------------------------
   Their own cooking. Three across, square, because these are photographs and
   nothing else: no likes, no restaurant, no caption. */
.kitchen-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  margin-top: 10px;
}

.kitchen-tile {
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 10px;
  background: var(--surface-2);
  display: block;
}

.kitchen-tile img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
