/*
 * ChefMate — quiet and editorial.
 *
 * The palette is "Graphite": a bright, faintly warm paper, a brown-black for
 * text, and an accent that is a warm dark grey rather than a colour — so
 * buttons read as part of the typography, and the pictures, which the prompt
 * composes on bright wood, are the only colour on the page. Chosen from
 * thirteen schemes in the same register (olive, slate, walnut, oxblood…) that
 * were tried side by side from a picker in Settings, and the picker went with
 * the losers: one palette for everyone. Warm saffron was the base before that
 * round, and lost out because a bright accent competed with the food.
 *
 * Green (`--herb`) is left for two things, both of which mean "settled, and
 * that is good news": a credit grant in the ledger, and an import flag the
 * cook has answered. Red (`--chilli`) is for errors, deletion and the budget
 * running hot. Everything else that was once green (a step's time, the
 * "current" tag, the ✓ on a finished trace step, a picked answer) is graphite
 * now: a colour on a control reads as meaning, and there was nothing green
 * meant.
 *
 * Theme-aware in all three states, same discipline as finapp: the complete
 * light palette on bare :root, dark redefined under BOTH prefers-color-scheme
 * (guarded so an explicit light choice wins) and [data-theme='dark'].
 *
 * The dark set is deliberately NOT black. The first cut sat the paper at
 * #131211 and the cards at #1c1a18 — L* 6 and 10 — and the whole page read as
 * a void with the pictures floating in it. This one lifts the surfaces two
 * steps (paper L* 13, card L* 18, sunk between them) and closes the gap to
 * the lines, so cards read as paper on a table rather than lights in a room;
 * the inks come down a touch to meet them, because pure-white text on a
 * lifted ground glares.
 */

:root {
  /* ---- surfaces */
  --paper: #f6f4f1;
  --card: #ffffff;
  --sunk: #edeae6;
  --line: #e2ddd6;
  --line-strong: #d1cac1;

  /* ---- ink */
  --ink: #1e1b18;
  --ink-soft: #4d4742;
  --muted: #7e7770;

  /* ---- accents. `--saffron` is the accent's name from the palette it
     replaced; every filled control and every emphasis reads it. */
  --saffron: #4a4440;
  --saffron-ink: #ffffff;
  --saffron-wash: #ebe7e2;
  --herb: #3f7a5e;
  --herb-wash: #e9f2ec;
  --chilli: #b8402e;
  --chilli-wash: #fae7e3;

  /* ---- shadows: warm, layered, never grey */
  --lift-1: 0 1px 2px rgba(60, 40, 25, 0.05), 0 2px 6px rgba(60, 40, 25, 0.04);
  --lift-2: 0 2px 4px rgba(60, 40, 25, 0.06), 0 8px 20px rgba(60, 40, 25, 0.07);
  --lift-3: 0 4px 8px rgba(60, 40, 25, 0.08), 0 18px 44px rgba(60, 40, 25, 0.12);

  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-xl: 28px;

  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  --ease: cubic-bezier(0.32, 0.72, 0.3, 1);

  /* ---- type: English. Hebrew and Russian override both below the faces. */
  --display: 'Newsreader', 'Iowan Old Style', Palatino, Georgia, serif;
  --body: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --paper: #22201e;
    --card: #2e2a27;
    --sunk: #282523;
    --line: #3c3833;
    --line-strong: #4e4944;

    --ink: #e8e3dd;
    --ink-soft: #c3bcb4;
    --muted: #9c948b;

    --saffron: #d9d3cb;
    --saffron-ink: #22201e;
    --saffron-wash: #3d3934;
    --herb: #7cbf9c;
    --herb-wash: #1f3128;
    --chilli: #ee8b73;
    --chilli-wash: #3b221c;

    /* Softer than before (0.4–0.5 alpha): on a lifted ground a black shadow
       reads as a hole under the card rather than a card above the page. */
    --lift-1: 0 1px 2px rgba(0, 0, 0, 0.25);
    --lift-2: 0 2px 6px rgba(0, 0, 0, 0.28), 0 10px 24px rgba(0, 0, 0, 0.22);
    --lift-3: 0 6px 14px rgba(0, 0, 0, 0.32), 0 22px 50px rgba(0, 0, 0, 0.28);
  }
}

:root[data-theme='dark'] {
  --paper: #22201e;
  --card: #2e2a27;
  --sunk: #282523;
  --line: #3c3833;
  --line-strong: #4e4944;

  --ink: #e8e3dd;
  --ink-soft: #c3bcb4;
  --muted: #9c948b;

  --saffron: #d9d3cb;
  --saffron-ink: #22201e;
  --saffron-wash: #3d3934;
  --herb: #7cbf9c;
  --herb-wash: #1f3128;
  --chilli: #ee8b73;
  --chilli-wash: #3b221c;

  /* Softer than before (0.4–0.5 alpha): on a lifted ground a black shadow
     reads as a hole under the card rather than a card above the page. */
  --lift-1: 0 1px 2px rgba(0, 0, 0, 0.25);
  --lift-2: 0 2px 6px rgba(0, 0, 0, 0.28), 0 10px 24px rgba(0, 0, 0, 0.22);
  --lift-3: 0 6px 14px rgba(0, 0, 0, 0.32), 0 22px 50px rgba(0, 0, 0, 0.28);
}

/*
 * The faces, one pair per language, chosen by looking at them in the app —
 * a picker in Settings offered a dozen candidates per language for a day,
 * and these won. Per language because a face that carries English headings
 * has no Hebrew and usually no Cyrillic (Fraunces, the previous display
 * face, had neither, so Russian headings were falling to Georgia the whole
 * time), and what the browser finds instead is nothing anyone chose.
 *
 *   English  Newsreader (headings) · Plus Jakarta Sans (text)
 *   Hebrew   Rubik (headings) · Heebo (text)
 *   Russian  the system serif and sans — Iowan Old Style / Palatino / Georgia
 *            for headings, the platform face for text; the candidates did
 *            not beat them.
 *
 * Self-hosted rather than pulled from Google's CDN: one fewer third-party
 * connection on the critical path, and the files are content-hashed by
 * build.mjs so they can be cached forever. Subsets only — the Hebrew faces
 * carry their Latin too, so an English word inside a Hebrew title is set in
 * the same face rather than the fallback's.
 */
@font-face {
  font-family: 'Newsreader';
  src: url('/fonts/newsreader-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Plus Jakarta Sans';
  src: url('/fonts/plus-jakarta-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Rubik';
  src: url('/fonts/rubik-hebrew.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
  font-family: 'Rubik';
  src: url('/fonts/rubik-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Heebo';
  src: url('/fonts/heebo-hebrew.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
@font-face {
  font-family: 'Heebo';
  src: url('/fonts/heebo-latin.woff2') format('woff2');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
    U+2215, U+FEFF, U+FFFD;
}

/* Which pair, by the language on the root — `setLocale` sets `lang`. */
:root[lang='he'] {
  --display: 'Rubik', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --body: 'Heebo', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
:root[lang='ru'] {
  --display: 'Iowan Old Style', Palatino, Georgia, serif;
  --body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/*
 * Direction as a NUMBER, for the handful of places a logical property cannot
 * reach — chiefly a transform, which has no logical form. Multiply any
 * translateX by this and it flips with the document.
 */
:root {
  --dir: 1;
}
:root[dir='rtl'] {
  --dir: -1;
}

* {
  box-sizing: border-box;
}

/*
 * The page is never wider than the screen. `clip` rather than `hidden` because
 * `hidden` turns the root into a scroll container, which breaks `position:
 * sticky` for everything inside it; `clip` only clips. This is the guarantee for
 * every screen at once — a long email in a flex row, an unbroken URL in a note,
 * a table someone adds next year — instead of a fix per offender. The admin
 * screen was the one that shipped with a horizontal scrollbar, intermittently,
 * and "intermittently" is the tell that this belongs at the root.
 */
html {
  overflow-x: clip;
}
body {
  /* A word that cannot fit breaks rather than overflows. `break-word`, not
     `anywhere`: it only intervenes when there is no other way. */
  overflow-wrap: break-word;
}

html,
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
}

body {
  font: 16px/1.55 var(--body);
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

/*
 * The display face. Newsreader carries an optical-size axis (6–72), and the
 * browser drives it from the font size when nothing pins it: at heading
 * sizes the contrast and the flare grow, in the small `.display` uses (a
 * dish's initial, a version number) it stays sturdy. Rubik has no such axis
 * and is unaffected.
 */
.display {
  font-family: var(--display);
  font-optical-sizing: auto;
  letter-spacing: -0.015em;
  line-height: 1.15;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
}

input,
textarea {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--saffron);
  outline-offset: 2px;
  border-radius: 6px;
}

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

/* ================================================================ shell */

.app {
  max-width: 720px;
  margin: 0 auto;
  min-height: 100dvh;
  /* Nothing fixed sits at the bottom any more; only the home indicator. */
  padding: 0 0 calc(var(--safe-b) + 16px);
}
/* The chat's composer is the bottom edge and pads for the indicator itself;
   the shell's padding under it would show as a strip of paper below the box
   at the end of the scroll. */
.app:has(> .chat) {
  padding-bottom: 0;
}

.pad {
  padding-inline: 18px;
}

/* ---------------------------------------------------------------- header */

.head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
  padding: calc(var(--safe-t) + 20px) 18px 14px;
}

/* A header with a title and nothing under it: the title and its tools sit on
   one centre line. `flex-end` is for the two-line block, where the sub-line
   is what should meet the buttons' bottom edge. */
.head.solo {
  align-items: center;
}
.head.solo h1 {
  line-height: 1;
}
.head h1 {
  margin: 0;
  /* Scales with the screen: the grid's header carries three controls now
     ("+ New" is a pill, not a circle) and at 32px "Your kitchen" wrapped onto
     two lines at 390px. 27px there, the full 32px from 470px up. */
  font-size: clamp(24px, 6.9vw, 32px);
  font-weight: 600;
}

.head .sub {
  margin: 3px 0 0;
  color: var(--muted);
  font-size: 14px;
}

/* The brand on the grid: the mark on its tile, the name beside it. The tile
   is the app icon's own white square — in dark mode that is a lit tile, as an
   app icon on a dark home screen is — with the icon's rounding and a lift so
   it sits on the page rather than in it. The wordmark is the display face at
   the header's size, tightened the way a logotype is. */
.brand {
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0;
  font-size: 0;
}
.brand svg {
  flex: none;
  border-radius: 26%;
  box-shadow: var(--lift-2);
}
.wordmark {
  font-size: clamp(24px, 6.9vw, 30px);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--ink);
}

/*
 * A pushed screen: back button, then the title. The title drops to 27px so
 * that its cap height sits level with the 40px button rather than towering
 * over it; the sub-line goes with it. Every screen that is not the grid
 * opens this way — there is no bar to get back with.
 */
.headback {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.headback h1 {
  font-size: 27px;
}

/* The grid's tools: reorder, new, settings. */
.headtools {
  display: flex;
  gap: 6px;
  margin-inline-start: auto;
  flex: none;
}
.iconbtn.add {
  /* The one filled circle in the row: the + alone, the accent behind it.
     It carried the word "New" for a while; the owner asked for the mark
     by itself (15 Sep 2026). */
  background: var(--saffron);
  border-color: transparent;
  color: var(--saffron-ink);
}

.iconbtn {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--lift-1);
  color: var(--ink-soft);
  transition: transform 0.15s var(--ease), color 0.15s var(--ease);
}
.iconbtn:active {
  transform: scale(0.93);
}
/* Undo with nothing to undo: there, and plainly not live. */
.iconbtn:disabled {
  opacity: 0.4;
  cursor: default;
}
.iconbtn.on {
  background: var(--saffron);
  border-color: transparent;
  color: var(--saffron-ink);
}

/* ---------------------------------------------------------------- search */

.searchwrap {
  position: relative;
  margin: 0 18px 16px;
}
.searchwrap svg {
  position: absolute;
  inset-inline-start: 14px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  pointer-events: none;
}
.search {
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  /* Logical: the icon sits at the inline start, so the room for it must too. */
  padding: 12px 16px;
  padding-inline-start: 42px;
  /* 16px or iOS zooms on focus — see the note on .composer textarea. */
  font-size: 16px;
  box-shadow: var(--lift-1);
}
.search::placeholder {
  color: var(--muted);
}
.search:focus {
  outline: none;
  border-color: var(--saffron);
  box-shadow: var(--lift-1), 0 0 0 3px var(--saffron-wash);
}

/* ---------------------------------------------------------------- chips */

.chiprow {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 0 18px 16px;
  scrollbar-width: none;
}
.chiprow::-webkit-scrollbar {
  display: none;
}

.chip {
  flex: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 8px 15px;
  font-size: 14px;
  color: var(--ink-soft);
  white-space: nowrap;
  transition: all 0.15s var(--ease);
}
.chip.on {
  /* The accent, not the text colour: `--ink` is a near-black and the accent a
     graphite, and a filled chip in one beside a button in the other read as
     two palettes on one screen. Same for `.btn` and `.msg.me` below. */
  background: var(--saffron);
  border-color: var(--saffron);
  color: var(--saffron-ink);
  font-weight: 550;
}

/* ================================================================ recipe grid */

.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
  padding: 0 18px;
}

@media (max-width: 339px) {
  .grid {
    grid-template-columns: 1fr;
  }
}

.rcard {
  /*
   * A column, so the PICTURE takes up any height the row leaves over.
   *
   * Two cards share a row and the grid stretches both to the taller; the
   * taller is the one whose title wrapped to three lines. The other used
   * to show the difference as a strip of empty card under its meta line —
   * dark, on the dark theme, and read as a broken image. Now the frame
   * below grows into it (`flex: 1`) and the picture, `object-fit: cover`
   * and centred, zooms towards its middle to fill the taller frame. Titles
   * may wrap as they like; the picture pays, never the layout.
   */
  display: flex;
  flex-direction: column;
  text-align: start;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--lift-1);
  transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.rcard:active {
  transform: scale(0.975);
  box-shadow: var(--lift-2);
}

/*
 * 4:3 on the grid. The first card in the feed spans both columns at 16:9 —
 * a magazine lead image, which is what stops a grid of equal tiles reading
 * like a settings screen.
 */
.rcard .shot {
  /* 4:3 is the MINIMUM; the frame grows to whatever the row leaves. */
  aspect-ratio: 4 / 3;
  flex: 1 0 auto;
  width: 100%;
  position: relative;
  overflow: hidden;
  background: var(--sunk);
}
.rbody {
  flex: none;
}
.rcard.lead {
  grid-column: 1 / -1;
}
/* The large view: one column, every card the lead card. */
.grid.large {
  grid-template-columns: 1fr;
}
/* Held to reorder — the browser must not select the title or offer its own menu. */
.rcard,
.rrow {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
.rcard.lead .shot {
  aspect-ratio: 16 / 9;
}
.rcard.lead .rbody h3 {
  font-size: 24px;
}

.shot img {
  /* Absolute, not `height: 100%`: a percentage height against a box sized by
     `aspect-ratio` is definite in the spec and has not always been in
     practice; `inset: 0` fills the frame whatever the engine thinks. */
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /*
   * Zoomed in. A generated picture is composed the way the prompt asks — a
   * plate on a table with room around it, three-quarter angle — which is
   * right on the recipe screen and small in a 180px card, where the dish
   * sat in the middle third and the card read as a table. The frame clips
   * the extra; the lead card is 16:9 and already crops enough.
   */
  transform: scale(1.15);
}
.rcard.lead .shot img {
  transform: none;
}

.rbody {
  padding: 12px 14px 14px;
}
.rbody h3 {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
/* The line under the name — what the dish is. Two lines at most on a card,
   one on a row; the name stays the name so twelve of them can be scanned. */
.rtag {
  margin: 3px 0 0;
  font-size: 13px;
  line-height: 1.35;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-wrap: pretty;
}
.rrtext .rtag {
  -webkit-line-clamp: 1;
  margin: 0;
}
/* nowrap + ellipsis: a wrapped meta row reads as broken, a clipped one does not. */
.rmeta {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 7px;
  font-size: 13px;
  color: var(--muted);
  flex-wrap: nowrap;
  overflow: hidden;
  white-space: nowrap;
}
.rmeta > span {
  overflow: hidden;
  text-overflow: ellipsis;
}
.dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
  opacity: 0.6;
}

/* ================================================================ recipe screen */

.hero {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 46dvh;
  background: var(--sunk);
}
.hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/*
 * Lets the white title card below overlap the image without a hard seam.
 *
 * `pointer-events: none`, because this is painted AFTER the hero's children
 * and covers the whole block: for a recipe with no picture it sat on top of
 * the "No picture yet" button and took every tap, and since the camera button
 * in the bar only exists once there IS a picture, a recipe that arrived
 * without one — every import — could not be given one from its own screen.
 * The bar escaped only because it has a z-index.
 */
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.28) 0%, transparent 34%);
}

.herobar {
  position: absolute;
  top: calc(var(--safe-t) + 12px);
  inset-inline: 12px;
  display: flex;
  justify-content: space-between;
  gap: 8px;
  z-index: 2;
}
.glassbtn {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(20, 12, 6, 0.42);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: #fff;
  transition: transform 0.15s var(--ease);
}
.glassbtn:active {
  transform: scale(0.9);
}
.herobar .right {
  display: flex;
  gap: 8px;
}

.titlecard {
  position: relative;
  margin: -26px 14px 0;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 20px 20px 18px;
  box-shadow: var(--lift-2);
  z-index: 1;
}
.titlecard h2 {
  margin: 0;
  font-size: 27px;
  font-weight: 600;
}
.titlecard .tagline {
  margin: 5px 0 0;
  color: var(--ink-soft);
  font-size: 15px;
  font-weight: 550;
  line-height: 1.4;
}
.titlecard .blurb {
  margin: 9px 0 0;
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1.5;
}

.facts {
  display: flex;
  gap: 10px;
  margin-top: 16px;
}
.fact {
  flex: 1;
  background: var(--sunk);
  border-radius: var(--r-md);
  padding: 10px 8px;
  text-align: center;
}
.fact .v {
  font-size: 15px;
  font-weight: 650;
}
.fact .k {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-top: 2px;
}

/* ---------------------------------------- the Ingredients ↔ Steps jump */

/*
 * The navigation the brief asks for. Sticky, so it stays reachable however far
 * down the steps you have scrolled — the whole point is jumping back to check
 * a quantity mid-cook with one thumb.
 */
.jump {
  position: sticky;
  top: 0;
  z-index: 5;
  margin: 22px 14px 0;
  padding: 5px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  background: var(--sunk);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: var(--lift-1);
}
.jump::before {
  content: '';
  position: absolute;
  /* Anchored to the INLINE start, so it begins under whichever button is
     first in reading order, and travels with --dir below. */
  inset-block: 5px;
  inset-inline-start: 5px;
  width: calc(50% - 7px);
  background: var(--card);
  border-radius: 999px;
  box-shadow: var(--lift-1);
  transition: transform 0.28s var(--ease);
}
/* translateX has no logical form, so --dir does the mirroring by hand. */
/* `data-second` says the pill is on the second of the two buttons. The same
   control serves Ingredients ↔ Steps on a recipe and Categories ↔ Cuisines
   on Explore, so the attribute names the position, not the section. */
.jump[data-second]::before {
  transform: translateX(calc((100% + 4px) * var(--dir)));
}
.jump button {
  position: relative;
  z-index: 1;
  padding: 9px 0;
  font-size: 14px;
  font-weight: 550;
  color: var(--muted);
  border-radius: 999px;
  transition: color 0.2s var(--ease);
}
.jump button[aria-current='true'] {
  color: var(--ink);
}

.section {
  margin: 26px 14px 0;
  scroll-margin-top: 74px;
}
.section > h3 {
  /* A heading, not a label. These were 13px uppercase tracking in the
     muted ink — the register of a form's field label — and on a phone the
     two words that organise the whole page read as small print. The
     display face at a real size, in the ink, and the caps dropped: a part's
     own name ("For the sauce") is a title in the recipe's language, which
     uppercase mangles in Hebrew (no case) and Russian (shouting). */
  margin: 0 0 12px;
  font-family: var(--display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0;
  color: var(--ink);
}

/* ---------------------------------------- ingredients */

/* A recipe's parts are one card each, and this wraps the lot of them: the
   Ingredients ↔ Steps pill has two destinations however many parts there are,
   so the observer watches this and the jump scrolls to it. The scroll margin
   is the section's, moved out here — without it the pill covers the first
   heading it just scrolled to. */
.ingblocks {
  scroll-margin-top: 74px;
}
.ingblocks .section:first-child {
  margin-top: 26px;
}

.ings {
  list-style: none;
  margin: 0;
  padding: 6px 4px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--lift-1);
}
.ings li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 11px 14px;
  font-size: 15px;
  border-bottom: 1px solid var(--line);
}
.ings li:last-child {
  border-bottom: 0;
}
/* Sections — "For the sauce", "For the dough" — inside the one Ingredients
   list. The outer li is a block that carries a heading and its own rows;
   the row styles above apply to the inner rows unchanged. */
.ings li.ingsection {
  display: block;
  padding: 0;
  border-bottom: 0;
}
.ingsection ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.ingsection + .ingsection {
  border-top: 1px solid var(--line);
}
.ingsub {
  margin: 0;
  padding: 12px 14px 4px;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--saffron);
  unicode-bidi: plaintext;
}
/* The proposal card's version of the same heading, spanning its two columns. */
.pring .prsub {
  display: block;
  grid-column: 1 / -1;
  margin-top: 4px;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--saffron);
  unicode-bidi: plaintext;
}
.ings .amt {
  flex: none;
  min-width: 4.5em;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--saffron);
}
/* Named .qnote, not .note: the page-level .note is a saffron banner, and an
   ingredient qualifier inheriting its background is not what anyone meant. */
.ings .qnote {
  color: var(--muted);
  font-size: 13px;
}

/* ---------------------------------------- steps */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 12px;
}
.step {
  display: grid;
  grid-template-columns: 34px 1fr;
  gap: 13px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 15px 16px;
  box-shadow: var(--lift-1);
}
.step .n {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--saffron-wash);
  color: var(--saffron);
  font-weight: 700;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.step p {
  margin: 5px 0 0;
  font-size: 15px;
  line-height: 1.6;
}
.step .time {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 9px;
  font-size: 12px;
  font-weight: 600;
  /* Was green, which on a page with no other green read as a status — "this
     step is fine" — when it is only how long the step takes. */
  color: var(--ink-soft);
  background: var(--saffron-wash);
  padding: 3px 9px;
  border-radius: 999px;
}
.step .tip {
  margin-top: 10px;
  padding: 9px 12px;
  background: var(--sunk);
  border-radius: var(--r-sm);
  font-size: 13px;
  color: var(--ink-soft);
  border-inline-start: 2px solid var(--saffron);
}

/* ---------------------------------------- sources */

.sources {
  display: grid;
  gap: 8px;
}
.src {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
}
.src .host {
  color: var(--muted);
  font-size: 12px;
  margin-top: 1px;
}

/* ================================================================ sheet */

.scrim {
  position: fixed;
  inset: 0;
  background: rgba(28, 18, 10, 0.42);
  backdrop-filter: blur(2px);
  z-index: 40;
  animation: fade 0.2s var(--ease);
}
@keyframes fade {
  from {
    opacity: 0;
  }
}

.sheet {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 41;
  max-height: 86dvh;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--lift-3);
  padding-bottom: var(--safe-b);
  animation: rise 0.3s var(--ease);
}
@media (min-width: 721px) {
  .sheet {
    max-width: 720px;
    margin: 0 auto;
  }
}
@keyframes rise {
  from {
    transform: translateY(100%);
  }
}

.sheet .grab {
  width: 38px;
  height: 4px;
  border-radius: 999px;
  background: var(--line-strong);
  margin: 10px auto 0;
  flex: none;
}
.sheet header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px 12px;
  flex: none;
}
.sheet header h3 {
  margin: 0;
  font-size: 21px;
  font-weight: 600;
}
.sheet .body {
  overflow-y: auto;
  padding: 0 18px 22px;
  -webkit-overflow-scrolling: touch;
}

/* ================================================================ history */

/* The timeline rail. `background` on the <ol> rather than a pseudo-element per
   row, so it is one continuous line and cannot develop gaps between items. */
.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  padding-inline-start: 26px;
  position: relative;
}
.timeline::before {
  content: '';
  position: absolute;
  inset-inline-start: 7px;
  top: 12px;
  bottom: 14px;
  width: 2px;
  background: var(--line);
}

.tl {
  position: relative;
  margin-bottom: 12px;
}
.tl::before {
  content: '';
  position: absolute;
  inset-inline-start: -26px;
  top: 15px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--paper);
  border: 3px solid var(--line-strong);
}
.tl.cur::before {
  border-color: var(--saffron);
  background: var(--saffron);
  box-shadow: 0 0 0 4px var(--saffron-wash);
}

.tlcard {
  width: 100%;
  text-align: start;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 14px 16px;
  box-shadow: var(--lift-1);
  transition: border-color 0.15s var(--ease);
}
.tl.cur .tlcard {
  border-color: var(--saffron);
}
.tltop {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.tlv {
  font-size: 15px;
  font-weight: 650;
}
.tldate {
  font-size: 12px;
  color: var(--muted);
}
.tlsum {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink-soft);
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--sunk);
  color: var(--muted);
}
.tag.ai {
  background: var(--saffron-wash);
  color: var(--saffron);
}
.tag.now {
  /* Filled, where the AI tag is a wash: "current" is the one that matters. */
  background: var(--saffron);
  color: var(--saffron-ink);
}

/* The cook's own words, marked as private wherever they appear. */
.remarks {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed var(--line-strong);
}
.remarks .lbl {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}
.remark {
  background: var(--sunk);
  border-radius: var(--r-sm);
  padding: 10px 12px;
  font-size: 14px;
  line-height: 1.5;
  margin-bottom: 8px;
}
.remark .when {
  display: block;
  margin-top: 5px;
  font-size: 11px;
  color: var(--muted);
}

.rinput {
  width: 100%;
  min-height: 76px;
  resize: vertical;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 11px 13px;
  /* 16px or iOS zooms on focus — see the note on .composer textarea. */
  font-size: 16px;
  line-height: 1.5;
}
.rinput:focus {
  outline: none;
  border-color: var(--saffron);
  box-shadow: 0 0 0 3px var(--saffron-wash);
}

/* ================================================================ buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 20px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 600;
  background: var(--saffron);
  color: var(--saffron-ink);
  transition: transform 0.15s var(--ease), opacity 0.15s var(--ease);
}
.btn:active {
  transform: scale(0.97);
}
.btn.accent {
  background: var(--saffron);
  color: var(--saffron-ink);
}
.btn.ghost {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  box-shadow: var(--lift-1);
}
.btn.wide {
  width: 100%;
}
.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ================================================================ chat */

.chat {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  /* The composer is in the flow and reserves its own room, safe area included. */
  padding-bottom: 0;
}
.msgs {
  flex: 1;
  display: grid;
  gap: 14px;
  padding: 6px 18px 18px;
  align-content: start;
}

.msg {
  max-width: 86%;
  font-size: 15px;
  line-height: 1.6;
  border-radius: var(--r-lg);
  padding: 12px 16px;
}
.msg.me {
  justify-self: end;
  background: var(--saffron);
  color: var(--saffron-ink);
  border-end-end-radius: 7px;
}
.msg.ai {
  justify-self: start;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--lift-1);
  border-end-start-radius: 7px;
}
.msg p {
  margin: 0 0 9px;
}
.msg p:last-child {
  margin-bottom: 0;
}
/*
 * A line in the conversation for something that HAPPENED rather than
 * something said — "Kept as version 3", "Saved as “Shakshuka”" — read back
 * from the stored thread. Centred and small, the way a chat app marks a
 * date, so it reads as a marker between messages and not as a message.
 */
.msgnote {
  justify-self: center;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  letter-spacing: 0.02em;
  padding: 2px 10px;
  unicode-bidi: plaintext;
}

/*
 * What the agent is DOING, streamed as it happens — searching, reading, drafting.
 * This is the visible difference between an agent and a chatbot, so it is a
 * first-class element rather than a spinner. See design §4.
 */
.trace {
  justify-self: start;
  max-width: 92%;
  display: grid;
  gap: 7px;
  background: var(--sunk);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 12px 15px;
}
.trace .t {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  color: var(--ink-soft);
}
.trace .t.done {
  color: var(--muted);
}
.trace .t svg {
  flex: none;
  color: var(--saffron);
}
.trace .t.live {
  color: var(--ink);
  font-weight: 550;
}
.spin {
  width: 14px;
  height: 14px;
  flex: none;
  border: 2px solid var(--line-strong);
  border-top-color: var(--saffron);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* A finished recipe, offered inside the conversation. */
.offer {
  justify-self: stretch;
  background: var(--card);
  border: 1px solid var(--saffron);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--lift-2);
}
.offer .shot {
  aspect-ratio: 16 / 9;
  background: var(--sunk);
}
.offer .oin {
  padding: 15px 16px 16px;
}
.offer h4 {
  margin: 0 0 5px;
  font-size: 20px;
  font-weight: 600;
}

/*
 * STICKY, not fixed.
 *
 * Fixed took the composer out of the flow, so the conversation scrolled behind
 * it and the last message ended up under the send button. The old fix was a
 * magic `padding-bottom` on `.chat` sized for a one-line box — which stopped
 * being right the moment the textarea grew towards its 128px maximum. Sticky
 * keeps it on screen AND in the layout, so the gap is always exactly its own
 * height, whatever that height is.
 */
.composer {
  position: sticky;
  inset-inline: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  gap: 9px;
  align-items: flex-end;
  /* On the screen's edge now, so the home indicator is its problem. */
  padding: 10px 14px calc(12px + var(--safe-b));
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
}
@media (min-width: 721px) {
  .composer {
    max-width: 720px;
    margin: 0 auto;
  }
}
/*
 * 16px, and not for looks: iOS Safari ZOOMS INTO any focusable field whose font
 * is smaller than that, and zooming scales the layout viewport. The symptom is
 * that tapping the composer makes the whole page wider than the screen and needs
 * horizontal scrolling to read — until you rotate the device, which resets the
 * zoom and hides the cause.
 *
 * The other way to stop it is `maximum-scale=1, user-scalable=no` in the viewport
 * meta, which also takes pinch-zoom away from everyone who needs it. Sizing the
 * text properly costs nothing.
 */
.composer textarea {
  flex: 1;
  min-height: 44px;
  /* `autoGrow` sets the height from the content; this is the ceiling, and it
     lives here so the cap and the growth cannot disagree. */
  max-height: 128px;
  overflow-y: auto;
  resize: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 22px;
  padding: 11px 16px;
  font-size: 16px;
  line-height: 1.4;
  box-shadow: var(--lift-1);
}
.composer textarea:focus {
  outline: none;
  border-color: var(--saffron);
}
.send {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: 50%;
  background: var(--saffron);
  color: var(--saffron-ink);
  transition: transform 0.15s var(--ease), opacity 0.15s var(--ease);
}

/*
 * A photograph with a message (PhotoAttach.tsx). The camera sits before the
 * box, the same circle as the send button in the card's colours; the strip
 * of what is attached rides above the composer, inside its blur, and each
 * thumbnail carries its own ×. In the bubble the photographs sit above the
 * words, two to a row at most — a fridge shot is read, not admired.
 */
.attach {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: none;
  border-radius: 50%;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--lift-1);
  color: var(--ink-soft);
}
.attach:disabled {
  opacity: 0.5;
}
.attachstrip {
  position: sticky;
  bottom: calc(66px + var(--safe-b));
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 8px 14px 0;
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.attached {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: var(--r-sm);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--lift-1);
}
.attached img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.attached button {
  position: absolute;
  top: 3px;
  inset-inline-end: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(30, 27, 24, 0.7);
  color: #fff;
}
.attacherr {
  font-size: 12.5px;
  color: var(--chilli);
}
.msg.me .mphotos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 6px;
  margin: -4px -8px 8px;
}
.msg.me .mphotos img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--r-sm);
  display: block;
}
/* A photograph with no words: the bubble is the pictures, no empty line under them. */
.msg.me .mphotos:last-child {
  margin-bottom: -4px;
}
.send:active {
  transform: scale(0.9);
}
.send:disabled {
  opacity: 0.4;
}

/* ================================================================ explore */

.cuisines {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  padding: 0 18px;
}
/* Inside a section the section's margin is the gutter already. */
.section .cuisines {
  padding: 0 4px;
}
.cui {
  position: relative;
  aspect-ratio: 5 / 4;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--lift-1);
  text-align: start;
  transition: transform 0.2s var(--ease);
}
.cui:active {
  transform: scale(0.975);
}
.cui img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.cui .veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(22, 13, 6, 0.82) 0%, rgba(22, 13, 6, 0.1) 55%, transparent 100%);
}
.cui .lab {
  position: absolute;
  inset-inline: 13px;
  bottom: 11px;
  color: #fff;
}
.cui .lab .flag {
  font-size: 19px;
  line-height: 1;
}
.cui .lab h4 {
  margin: 5px 0 0;
  font-size: 17px;
  font-weight: 600;
}
.cui .lab .cnt {
  font-size: 11px;
  opacity: 0.78;
  margin-top: 1px;
}

.dishes {
  display: grid;
  gap: 10px;
  padding: 0 18px;
}
.dish {
  display: flex;
  align-items: center;
  gap: 13px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 10px;
  box-shadow: var(--lift-1);
  text-align: start;
  width: 100%;
}
.dish .thumb {
  width: 62px;
  height: 62px;
  flex: none;
  border-radius: var(--r-md);
  object-fit: cover;
  background: var(--sunk);
}
.dish .dmeta {
  min-width: 0;
  flex: 1;
}
/* A dish Commons has no usable photograph of: its initial, set like a drop cap. */
.dish .thumb.noshot {
  position: static;
  display: grid;
  place-items: center;
  font-family: var(--display);
  font-size: 26px;
  color: var(--saffron);
  background: var(--sunk);
}
.dish:disabled {
  opacity: 0.7;
}
/* Tiles while the index loads: the same shape, no content, a slow breath. */
.cui.skeleton {
  background: var(--sunk);
  border-color: transparent;
  box-shadow: none;
  animation: breathe 1.6s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}
.dish h4 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 1.25;
}
/*
 * The line under the name says what the dish IS. Two lines and then it is cut
 * — a line-clamp rather than nowrap, because "Eggs poached in spiced tomato and
 * pepper sauce" does not fit one phone line and cutting it at "spiced" tells
 * less than the photograph did.
 */
.dish .what {
  margin: 3px 0 0;
  font-size: 13px;
  line-height: 1.35;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-wrap: pretty;
}
/* The affordance: a chevron in the reading direction, quiet until pressed. */
.dish .go {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  color: var(--muted);
  transform: scaleX(var(--dir));
}
.dish:active .go {
  color: var(--ink);
}
/* Search results only: where the dish is from, above the name, small. */
.dish .kicker {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--saffron);
  margin-bottom: 2px;
}

/* ================================================================ usage */

.meter {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 20px;
  box-shadow: var(--lift-1);
  margin: 0 18px 14px;
}
.meter .big {
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.meter .of {
  font-size: 15px;
  color: var(--muted);
  margin-inline-start: 4px;
}
.bar {
  height: 9px;
  border-radius: 999px;
  background: var(--sunk);
  overflow: hidden;
  margin-top: 14px;
}
.bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--saffron);
  transition: width 0.5s var(--ease);
}
.bar.hot i {
  background: linear-gradient(90deg, var(--saffron), var(--chilli));
}

.split {
  display: grid;
  gap: 10px;
  margin-top: 16px;
}
.srow {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
}
.swatch {
  width: 9px;
  height: 9px;
  border-radius: 3px;
  flex: none;
}
.srow .nm {
  flex: 1;
  color: var(--ink-soft);
}
.srow .amt {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.calls {
  display: grid;
  gap: 8px;
  margin: 0 18px;
}
.call {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 12px 14px;
}
.call .what {
  flex: 1;
  min-width: 0;
}
.call .what b {
  display: block;
  font-size: 14px;
  font-weight: 600;
}
.call .what span {
  font-size: 12px;
  color: var(--muted);
}
.call .cost {
  font-variant-numeric: tabular-nums;
  font-size: 14px;
  font-weight: 650;
}

/* ================================================================ misc */

.empty {
  text-align: center;
  padding: 56px 32px;
  color: var(--muted);
}
.empty h3 {
  margin: 14px 0 6px;
  font-size: 20px;
  color: var(--ink);
  font-weight: 600;
}
.empty p {
  margin: 0;
  font-size: 14px;
  line-height: 1.6;
}

.note {
  margin: 0 18px 16px;
  padding: 11px 14px;
  background: var(--saffron-wash);
  border-radius: var(--r-md);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-soft);
  border-inline-start: 3px solid var(--saffron);
}

/* ================================================================ categories */

/*
 * The breadcrumb. Its own row above the chips rather than a chip inside them,
 * because "where am I" and "where can I go next" are different questions and
 * mixing them into one scrolling row makes both harder to answer.
 */
.crumbs {
  display: flex;
  align-items: center;
  gap: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  padding: 0 18px 10px;
}
.crumbs::-webkit-scrollbar {
  display: none;
}
.crumbstep {
  display: flex;
  align-items: center;
  gap: 2px;
  white-space: nowrap;
}
.crumb {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  border-radius: 8px;
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
  unicode-bidi: plaintext;
}
.crumb[aria-current='true'] {
  color: var(--ink);
  font-weight: 650;
}
/* The separator is a glyph that points, so it mirrors with the document. */
.crumbs .sep {
  color: var(--line-strong);
  font-style: normal;
  font-size: 14px;
}
:root[dir='rtl'] .crumbs .sep {
  transform: scaleX(-1);
}

/* The rolled-up count sitting inside a category chip. */
.cnum {
  margin-inline-start: 7px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--sunk);
  color: var(--muted);
  font-size: 11px;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.chip.on .cnum {
  background: rgba(255, 255, 255, 0.16);
  color: inherit;
}

/* ---------------------------------------- where a recipe is filed */

.catsummary {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  width: 100%;
  margin-top: 14px;
  text-align: start;
}
.catchip {
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--saffron-wash);
  color: var(--saffron);
  font-size: 12px;
  font-weight: 600;
  unicode-bidi: plaintext;
}
.catchip.empty {
  background: var(--sunk);
  color: var(--muted);
  font-weight: 500;
}
.catedit {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--sunk);
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
}

/* ---------------------------------------- the picker tree */

.cattree {
  display: grid;
  gap: 4px;
}

/*
 * Indentation is padding on the INLINE start, driven by a --depth custom
 * property, so it mirrors in RTL without a second set of rules.
 */
.catrow {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-inline-start: calc(var(--depth, 0) * 20px);
}

.cattoggle {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 11px 13px;
  text-align: start;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.cattoggle.on {
  border-color: var(--saffron);
  background: var(--saffron-wash);
}
.tick {
  display: grid;
  place-items: center;
  width: 19px;
  height: 19px;
  flex: none;
  border-radius: 6px;
  border: 1.5px solid var(--line-strong);
  background: var(--card);
  color: var(--saffron-ink);
}
.cattoggle.on .tick {
  background: var(--saffron);
  border-color: var(--saffron);
}
.catname {
  font-size: 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  unicode-bidi: plaintext;
}

.catadd {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 50%;
  border: 1px dashed var(--line-strong);
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
}

/* Holds the add-button's place on rows that cannot have one — see CategorySheet. */
.catadd-spacer {
  width: 34px;
  flex: none;
}

.catnew {
  margin-top: 14px;
  padding: 14px;
  background: var(--sunk);
  border-radius: var(--r-lg);
}
.catnewlabel {
  margin: 0 0 9px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  unicode-bidi: plaintext;
}
.catnewbtns {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}
.catnewbtns .btn {
  flex: 1;
}

/* ================================================================ language */

/*
 * First-run language choice. Full screen, because the answer mirrors the whole
 * layout — asking inside the app means the user watches it flip.
 */
.langpick {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.langinner {
  width: 100%;
  max-width: 340px;
  text-align: center;
}
.langmark,
.gatemark {
  display: flex;
  justify-content: center;
}
.langmark svg,
.gatemark svg {
  border-radius: 22%;
  box-shadow: var(--lift-2);
}
.langinner h1 {
  margin: 14px 0 26px;
  font-size: 38px;
  font-weight: 600;
}

.langlist {
  display: grid;
  gap: 10px;
  margin-bottom: 22px;
}
.langopt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 16px 20px;
  font-size: 21px;
  box-shadow: var(--lift-1);
  color: var(--ink-soft);
  transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.langopt.on {
  border-color: var(--saffron);
  color: var(--ink);
  box-shadow: var(--lift-1), 0 0 0 3px var(--saffron-wash);
}
.langopt svg {
  color: var(--saffron);
  flex: none;
}
.langnote {
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--muted);
}

/* ================================================================ credits */

.balrow {
  display: flex;
  align-items: baseline;
  gap: 7px;
  flex-wrap: wrap;
}
.heldnote,
.buys {
  margin: 10px 0 0;
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
}
.mhead {
  margin: 0 0 10px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
.secthead {
  margin: 22px 18px 10px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}
/* A credit grant is good news and reads as such; a spend is the default. */
.call .cost.pos {
  color: var(--herb);
}

/* ================================================================ sheets */

.sheetsub {
  margin: 2px 0 0;
  font-size: 13px;
  color: var(--muted);
}
/* ---------------------------------------- share targets */

.sheetsub {
  margin: 2px 0 0;
  font-size: 13px;
  color: var(--muted);
}
/* Four tiles, the phone's own share-row idiom: a round mark, a label under
   it. The marks keep their brands' colours; Copy is the one in ours. */
.shareapps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-top: 6px;
}
.shareapp {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 8px 2px;
  border-radius: var(--r-md);
  color: var(--ink);
  text-decoration: none;
  transition: transform 0.15s var(--ease), background 0.15s var(--ease);
}
.shareapp:active {
  transform: scale(0.96);
  background: var(--sunk);
}
.shareapp:disabled,
.shareapp.waiting {
  opacity: 0.5;
}
.appmark {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  box-shadow: var(--lift-1);
  color: #fff;
}
.appmark.whatsapp {
  background: #25d366;
}
.appmark.telegram {
  background: #2aabee;
}
.appmark.gmail {
  background: #ffffff;
  border: 1px solid var(--line);
}
.appmark.copy {
  background: var(--saffron);
  color: var(--saffron-ink);
}
.appmark .spin {
  width: 18px;
  height: 18px;
  border-color: rgba(255, 255, 255, 0.5);
  border-top-color: #fff;
}
.applabel {
  font-size: 12px;
  font-weight: 550;
  text-align: center;
}

.sheetnote {
  margin: 10px 2px 20px;
  font-size: 13px;
  color: var(--muted);
  line-height: 1.55;
}
.minihead {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 9px;
  font-size: 12px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
}

.tagrow {
  display: flex;
  gap: 6px;
  margin-bottom: 9px;
  flex-wrap: wrap;
}

/* ================================================================ rtl fixes */

/*
 * The stylesheet is written in logical properties, so the layout mirrors on its
 * own. These are the leftovers that cannot: things whose content is not in the
 * document's language, and one glyph that points.
 */

/* The back chevron points at the start of the line, so it must flip. Icons that
   are not directional — clock, share, search — must NOT, hence the narrow
   selector rather than mirroring every icon in the bar. */
:root[dir='rtl'] .glassbtn:first-child svg,
:root[dir='rtl'] .iconbtn svg.back,
:root[dir='rtl'] .btn svg.back,
:root[dir='rtl'] .iconbtn svg.undo,
/* The send arrow points the way the text runs. In Hebrew that is leftwards, and
   an arrow pointing back at the words you just typed reads as "undo". */
:root[dir='rtl'] .send svg {
  transform: scaleX(-1);
}

/* Latin content inside a Hebrew page: a URL, a hostname, a monospace block.
   These stay LTR or they read backwards. */
:root[dir='rtl'] .src .host,
:root[dir='rtl'] .sharepre {
  direction: ltr;
  text-align: start;
}

/*
 * Bidi, for everything whose language is not the interface's language.
 *
 * Two tools, and the difference matters:
 *
 * `plaintext` resolves a run's direction from its OWN first strong character,
 * exactly as the Unicode algorithm treats a standalone paragraph. It is the
 * right tool for anything a person wrote — a recipe title, a step, a remark —
 * and for values that mix digits with a Latin or Hebrew word, like "4 tbsp" or
 * "1 שע׳ 5 דק׳". Without it an English sentence in a Hebrew page has its full
 * stop moved to the front (".everything else is seasoning") and an amount
 * renders as "tbsp 4", because neutral characters take the paragraph's
 * direction rather than the sentence's.
 *
 * `direction: ltr` is for runs with NO strong character at all, where there is
 * nothing for plaintext to resolve from — a signed number is the case here.
 *
 * Applied to the containers of such content, never to the page body: the layout
 * itself must still mirror. `text-align: start` comes along because plaintext
 * sets the direction of the text, not the alignment of the box.
 */
.rbody h3,
.inl,
.titlecard h2,
.titlecard .tagline,
.titlecard .blurb,
.rtag,
.otag,
.rmeta > span,
.fact .v,
.ings li,
.ings .amt,
.step p,
.step .tip,
.step .time,
.tag,
.tlsum,
.remark,
.msg,
.trace .t,
.offer h4,
.offer .oin p,
.dish h4,
.dish p,
.cui .lab h4,
.call .what b,
.src {
  unicode-bidi: plaintext;
  text-align: start;
}

/* The fact tiles are centred by design; they need plaintext's DIRECTION
   resolution, not its start-alignment, which the group rule above would
   otherwise impose on them. */
.fact .v,
.fact .k {
  text-align: center;
}

/*
 * U+2212 is a bidi "European separator", so in an RTL line it detaches from its
 * digits and lands after them — a 59-credit charge rendering as "59−", which
 * reads as a positive number with a stray dash. There is no strong character
 * for plaintext to work from, so the direction is set outright. Numbers are
 * written left-to-right in Hebrew too, so this is correct, not a workaround.
 */
.call .cost {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: end;
}

/* ================================================================ settings rows */

.rows {
  display: grid;
  /* Not a bare auto track: that is as wide as the widest row's content, and
     the Russian settings rows ("Список рецептов" beside three segments)
     laid out at 396px in a 390px viewport. Same lesson as the reorder list. */
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  margin: 0 18px;
}
.rowitem,
.rowlink {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 13px 15px;
  color: var(--ink-soft);
  text-align: start;
}
.rowlink {
  background: var(--sunk);
  border-color: transparent;
}
.rowlabel {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  color: var(--ink);
}
/* A settings row whose control does not fit beside its label wraps the
   control onto its own line, at the inline end. Without this the label took
   what was left and broke mid-word — "Оформле / ние" beside the three
   appearance segments at 390px. The label's floor is its longest word, so
   the row wraps instead of the word. */
.rowitem {
  flex-wrap: wrap;
}
.rowitem .rowlabel {
  min-width: min-content;
}
.rowitem .segsmall {
  margin-inline-start: auto;
}
/* Points along the reading direction. */
:root[dir='rtl'] .rowlink > svg:last-child,
:root[dir='rtl'] .catsummary .catedit {
  transform: scaleX(-1);
}

/* A two-option switch small enough to sit at the end of a settings row. */
.segsmall {
  display: flex;
  gap: 3px;
  flex: none;
  padding: 3px;
  background: var(--sunk);
  border-radius: 999px;
}
.segsmall button {
  padding: 5px 11px;
  border-radius: 999px;
  font-size: 13px;
  color: var(--muted);
  unicode-bidi: plaintext;
}
.segsmall button.on {
  background: var(--card);
  color: var(--ink);
  font-weight: 600;
  box-shadow: var(--lift-1);
}

/* ================================================================ translate */

.quote {
  display: flex;
  align-items: center;
  gap: 13px;
  background: var(--saffron-wash);
  border-radius: var(--r-lg);
  padding: 16px;
  color: var(--saffron);
}
.quotebig {
  font-size: 20px;
  font-weight: 700;
  color: var(--ink);
  unicode-bidi: plaintext;
}
.quotesub {
  font-size: 13px;
  color: var(--ink-soft);
  margin-top: 2px;
}

/* ================================================================ import */

.bigopt {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 16px;
  margin-bottom: 10px;
  box-shadow: var(--lift-1);
  text-align: start;
  transition: transform 0.15s var(--ease), border-color 0.15s var(--ease);
}
.bigopt:active {
  transform: scale(0.98);
}
/* An option that is a link (the shared page's "open the app") looks like the others. */
a.bigopt {
  color: inherit;
  text-decoration: none;
}
.bigicon {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  flex: none;
  border-radius: var(--r-md);
  background: var(--saffron-wash);
  color: var(--saffron);
}
.bigtext {
  min-width: 0;
}
.bigtext b {
  display: block;
  font-size: 16px;
  font-weight: 600;
}
.bigtext span {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  color: var(--muted);
}

.srcnote {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 10px 13px;
  background: var(--sunk);
  border-radius: var(--r-md);
  font-size: 13px;
  color: var(--muted);
  margin-bottom: 14px;
}
a.srcnote {
  color: inherit;
  text-decoration: none;
}
.srcnote span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  direction: ltr;
  unicode-bidi: isolate;
}

/* ---------------------------------------- flagged content */

/*
 * A flag and the three ways out of it, against the row it is about.
 *
 * The suggestion used to be a card at the FOOT of the section, four or five
 * rows below the ingredient it answered, and nobody read them together. This
 * block is the warning AND the choices, under the row, at 90% of its width
 * and pushed to the row's end edge so the indent says "about the line above".
 * 90% rather than 100% because a block flush with the row reads as the next
 * row.
 */
.resolve {
  flex: 0 0 90%;
  width: 90%;
  margin-block-start: 10px;
  margin-inline-start: auto;
  padding: 11px 13px;
  background: var(--card);
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--muted);
  border-radius: var(--r-md);
}
.resolve.blocking {
  border-inline-start-color: var(--chilli);
  background: var(--chilli-wash);
}
.resolve.assumed:not(.blocking):not(.settled) {
  border-inline-start-color: var(--saffron);
}
/* Answered. Green is otherwise reserved for a credit grant (see the note at
   the top of this file); it earns a second use here because "this one is
   dealt with" is the one thing the cook is scanning a flagged draft for, and
   a row they have settled should not still look like a warning. */
.resolve.settled {
  border-inline-start-color: var(--herb);
  background: var(--herb-wash);
}
.resolvehead {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 6px;
  color: var(--ink-soft);
}
.resolve.settled .resolvehead {
  color: var(--herb);
}
.issuekind {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}
.issuemsg {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  unicode-bidi: plaintext;
}
.choosehint {
  margin: 10px 0 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}
.choices {
  display: grid;
  gap: 6px;
  margin-top: 8px;
}
/* One of these, and changeable: a radio group, not three buttons that each
   do something. The tick well is always there, so picking one does not shift
   the labels sideways. */
.choice {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  width: 100%;
  padding: 9px 11px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  text-align: start;
  transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
.choice:active {
  transform: scale(0.99);
}
.choice.on {
  border-color: var(--herb);
  background: var(--card);
}
.choice .tick {
  display: grid;
  place-items: center;
  flex: none;
  width: 19px;
  height: 19px;
  margin-top: 1px;
  border: 1.5px solid var(--line-strong);
  border-radius: 50%;
  color: var(--saffron-ink);
}
.choice.on .tick {
  background: var(--herb);
  border-color: var(--herb);
}
.choicetext {
  min-width: 0;
}
.choicetext b {
  display: block;
  font-size: 14px;
  font-weight: 600;
}
/* The suggestion itself, as the body of the option that takes it — it is the
   answer, not a paragraph somewhere above the answers. */
.choicetext span {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-soft);
  unicode-bidi: plaintext;
}
/* The one that was picked keeps its colour; the others step back rather than
   disappear, because changing your mind is a tap on one of them. */
.resolve.settled .choice:not(.on) {
  opacity: 0.55;
}

/* On a row, the ROW carries the tint and the block carries only the coloured
   edge — a red-washed card inside a red-washed row read as two warnings, one
   nested in the other. A flag about the recipe as a whole has no row under
   it, so it keeps its own. Placed after the tinted rules on purpose: source
   order decides the tie. */
.ings li > .resolve,
.step > .resolve {
  background: var(--card);
}
/* The step is a grid; without this the block sits in whichever column it
   lands in rather than under the whole step. */
.step > .resolve {
  grid-column: 1 / -1;
}

.ings li.flagged,
.step.flagged {
  background: var(--chilli-wash);
}
.step.flagged {
  border-color: var(--chilli);
}
.ings li.settled,
.step.settled {
  background: var(--herb-wash);
}
.step.settled {
  border-color: var(--herb);
}

/* ---------------------------------------- the importer, working */

/*
 * A revision is the whole recipe back through the model — eight to fifteen
 * seconds — and it used to run behind a spinner on the send button with the
 * stale draft readable and tappable underneath. The screen is held instead:
 * nothing to tap, one thing to read, and the instruction repeated so the
 * wait is attached to what was asked.
 */
.scrim.working {
  z-index: 44;
}
.workcard {
  position: fixed;
  z-index: 45;
  inset-inline: 0;
  top: 50%;
  translate: 0 -50%;
  width: min(340px, calc(100% - 48px));
  margin-inline: auto;
  display: grid;
  justify-items: center;
  gap: 10px;
  padding: 26px 22px;
  background: var(--paper);
  border-radius: var(--r-xl);
  box-shadow: var(--lift-3);
  text-align: center;
  animation: rise 0.25s var(--ease);
}
.workcard b {
  font-size: 17px;
  font-weight: 600;
}
.workwhat {
  margin: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-soft);
  unicode-bidi: plaintext;
}
.workwhat::before {
  content: '“';
}
.workwhat::after {
  content: '”';
}
.worknote {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
}

/* What the revision did, named rather than left to be found by re-reading. */
.changelist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}
.changelist li {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 10px 12px;
  background: var(--sunk);
  border-radius: var(--r-sm);
  font-size: 14px;
  line-height: 1.45;
  unicode-bidi: plaintext;
}
.changelist svg {
  flex: none;
  margin-top: 2px;
  color: var(--herb);
}

/* ================================================================ reordering */

.orderlist {
  display: grid;
  /*
   * `minmax(0, 1fr)`, not the implicit `auto` track. An auto track sizes to
   * its content's max-content, and a row's max-content is its title at full
   * length — the `min-width: 0` on the title only lets it shrink INSIDE the
   * row, it does not shrink what the row asks the grid for. One long title
   * made the whole page wider than the screen: measured with "בורשט צמחוני
   * עשיר עם שורשים לבנים, פטריות ושעועית" in a 386px viewport, the document
   * laid out at 438px; on the phone the header and every row were cut at
   * the edge.
   */
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  padding: 0 18px;
}

.orow {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 9px 12px;
  box-shadow: var(--lift-1);
  /* Only the non-dragged rows animate; the dragged one follows the finger and
     must not lag behind it. */
  transition: transform 0.18s var(--ease);
}
.orow.dragging {
  transition: none;
  box-shadow: var(--lift-3);
  border-color: var(--saffron);
  position: relative;
}

.ohandle {
  display: grid;
  place-items: center;
  width: 34px;
  height: 40px;
  flex: none;
  color: var(--line-strong);
  /* Without this the page scrolls under the drag on touch. */
  touch-action: none;
  cursor: grab;
}
.ohandle:active {
  cursor: grabbing;
  color: var(--saffron);
}

.othumb {
  width: 42px;
  height: 42px;
  flex: none;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--sunk);
}
.othumb.big {
  width: 56px;
  height: 56px;
  position: relative;
  color: var(--muted);
}
.othumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* See .shot img: at 42px a plate with table round it reads as a table. */
  transform: scale(1.25);
}
/* The rows view. Same row as the reorder list, without the handle. */
.rowlist {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
  padding: 0 18px;
}
.rrow {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 9px 12px;
  box-shadow: var(--lift-1);
  text-align: start;
  transition: transform 0.15s var(--ease);
}
.rrow:active {
  transform: scale(0.985);
}
.rrtext {
  flex: 1;
  min-width: 0;
  display: grid;
  gap: 3px;
}
.rrtext .otitle {
  flex: none;
  font-size: 16px;
}
.rrtext .rmeta {
  margin: 0;
}
.otitle {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  unicode-bidi: plaintext;
}

/* ================================================================ category rename */

.catedit-btn {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 50%;
  color: var(--muted);
}
.catrename {
  flex: 1;
  min-width: 0;
  padding: 10px 13px;
  unicode-bidi: plaintext;
}

/* ================================================================ the gate */

/*
 * Sign-in, and the states around it. Deliberately its own layout rather than a
 * card inside the app shell: there is no tab bar to clear and nothing to scroll,
 * so the whole viewport belongs to one decision.
 */
.gate {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.gateinner {
  width: 100%;
  max-width: 340px;
  text-align: center;
}
.gateinner h1 {
  margin: 14px 0 10px;
  font-size: 38px;
  font-weight: 600;
}
.gatebody {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-soft);
}
.gateemail {
  margin: 14px 0 0;
  font-size: 13px;
  color: var(--muted);
  direction: ltr;
  unicode-bidi: isolate;
}
.gatenote {
  margin: 18px 0 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
}
.gateerr {
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--chilli);
}
/* The Google mark is a fixed brand asset and must not be recoloured. */
.gsi svg {
  flex: none;
}

/* ---------------------------------------- admin rows */

.uemail {
  display: block;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  direction: ltr;
  unicode-bidi: plaintext;
}
.umeta {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 3px;
  font-size: 12px;
  color: var(--muted);
}

/* ---------------------------------------- test users */

/* The name is the cook's own text; the row's language is not necessarily its
   language, so it decides its own direction (rule 26). */
.tuname {
  display: block;
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  unicode-bidi: plaintext;
}
/* Three actions beside a name leave the name 100px at 390px wide — "שרה בת
   שבע…" and the date on two lines. Below 480px the actions take a line of
   their own, at the inline end where the one action on a member row sits. */
.turow {
  flex-wrap: wrap;
}
.tubtns {
  display: flex;
  gap: 6px;
  margin-inline-start: auto;
}
@media (max-width: 480px) {
  .tubtns {
    flex-basis: 100%;
    justify-content: flex-end;
  }
}
.tubtns .btn {
  padding: 8px 12px;
  font-size: 13px;
}
.tudel {
  color: var(--chilli);
}
/* The new-name field carries no search icon, so no room for one. */
.tunew {
  padding-inline-start: 16px;
  box-shadow: none;
}
/* Inside a test user: the one row in Settings that says so. */
.note.acting {
  border-inline-start-color: var(--saffron);
}

/* ---------------------------------------- import: composing the sources */

.compose .srchead {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 20px 0 9px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.compose .srchead:first-child {
  margin-top: 4px;
}
.srccount {
  margin-inline-start: auto;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  font-variant-numeric: tabular-nums;
}
/* Thumbnails of what will be sent, and the two ways to add one. Squares in
   a row that wraps: ten photographs are two rows, not a scroll. */
.shots {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.shot,
.shotadd {
  position: relative;
  width: 104px;
  height: 104px;
  border-radius: var(--r-md);
  overflow: hidden;
  flex: none;
}
/* The add-a-link tile: the same tile, the row's width, and short. */
.shotadd.wide {
  width: 100%;
  height: 56px;
  grid-auto-flow: column;
  gap: 8px;
  font-size: 14px;
}
.shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.shotx {
  position: absolute;
  top: 4px;
  inset-inline-end: 4px;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(20, 12, 6, 0.62);
  color: #fff;
}
.shotadd {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 6px;
  background: var(--card);
  border: 1px dashed var(--line-strong);
  color: var(--ink-soft);
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
}
.shotadd:active {
  background: var(--sunk);
}
.shotadd span {
  padding-inline: 4px;
}
.shotadd input {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
.linkrow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  margin-bottom: 6px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  font-size: 14px;
}
.linkrow > span {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  unicode-bidi: plaintext;
}
.search.plain {
  padding-inline-start: 16px;
  box-shadow: none;
}

/* ---------------------------------------- import: the draft, editable */

/* An input that reads as the text it holds until it is focused. The review
   is the recipe, not a form; a wrong "20g" is fixed by tapping it. 16px or
   iOS zooms on focus (rule 27) — the smaller-looking ones are scaled, not
   sized. */
.inl {
  width: 100%;
  min-width: 0;
  padding: 2px 4px;
  margin: -2px -4px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  font: inherit;
  font-size: 16px;
  color: inherit;
  unicode-bidi: plaintext;
}
.inl:focus {
  outline: none;
  background: var(--saffron-wash);
}
.inl::placeholder {
  color: var(--muted);
}
.titleedit {
  display: grid;
  gap: 4px;
  margin-bottom: 12px;
}
.inl.title {
  font-size: 24px;
  font-weight: 600;
  line-height: 1.15;
  color: var(--ink);
}
.inl.tagline {
  font-weight: 550;
  color: var(--ink-soft);
}
.ings.editable li {
  align-items: flex-start;
  /* The resolution block sits on its own line under the row it belongs to. */
  flex-wrap: wrap;
}
.itemcol,
.stepcol {
  flex: 1;
  min-width: 0;
}
/* Under the item, not after it: the qualifier used to trail the ingredient
   name and wrapped as a dangling dot on the next line. */
.ings.editable .qnote {
  display: block;
  margin-top: 2px;
}
.steps.editable .step {
  grid-template-columns: 34px 1fr auto;
}
.rinput.tall {
  min-height: 120px;
}
/* Amount and unit are one thought, so they share a line; the ingredient, the
   note and the part each get the full width. Deliberately NOT a `.frow`:
   that class is defined 200 lines further down with a 1fr 5rem 5rem template
   and would win the tie on source order, which is how the unit field first
   came out at 5rem with "tbsp…" clipped inside it. */
.amtrow {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  align-items: end;
}
.amtrow .flabel {
  margin-top: 16px;
}

/* The pencil and the × at a row's end. Muted until they are wanted. */
.rowtools {
  display: flex;
  flex: none;
  gap: 2px;
}
.rowx {
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  color: var(--muted);
}
.rowx:active {
  background: var(--sunk);
}
.rowx.done {
  color: var(--saffron-ink);
  background: var(--saffron);
}
.addrow {
  justify-content: center;
}
.addrow .link,
.steps .addrow .link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 14px;
  color: var(--ink-soft);
}
.steps .addrow {
  display: flex;
  justify-content: center;
  padding: 4px 0 0;
}
/* Telling the importer what to change: the chat's composer, in miniature. */
.askbar {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 6px 6px 6px 14px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 26px;
  box-shadow: var(--lift-1);
}
.asktext {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  resize: none;
  font: inherit;
  font-size: 16px;
  line-height: 1.45;
  padding: 9px 0;
  max-height: 120px;
  field-sizing: content;
}
.asktext:focus {
  outline: none;
}

/* ================================================================ painting */

/*
 * A picture being made, in the space it will occupy (Generating.tsx).
 *
 * In the look the picture will have — HERO_ATMOSPHERE in prompts.ts: a
 * scrubbed farmhouse table, warm late-afternoon window light from the side,
 * long soft shadows. So the ground is worn wood, two soft pools of it — a
 * deep walnut and a honeyed board — drifting on slow, unequal periods, and
 * a warm bar of light crosses from the start side the way the window's
 * would, slowly. Nothing bright: the first version drifted tomato, butter
 * and herb across the box, and the owner's note was that those were not
 * the colours of any picture the app makes (14 Sep 2026); a blob of red
 * promised a plate the painting never delivered. Everything is transform
 * and opacity, so it composites without layout; the blur is on the pools,
 * not the box, so the caption stays sharp.
 */
.painting {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: #8a6a4b; /* the table, mid-tone; the pools shade it */
  isolation: isolate;
}
.paint {
  position: absolute;
  width: 70%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(34px);
  opacity: 0.85;
  will-change: transform;
}
.paint.a {
  background: #4a3120; /* walnut: the shadowed end of the table */
  top: -25%;
  inset-inline-end: -15%;
  animation: drift-a 13s ease-in-out infinite alternate;
}
.paint.b {
  background: #c9995e; /* honey: the board the light falls on */
  bottom: -30%;
  inset-inline-start: -12%;
  animation: drift-b 11s ease-in-out infinite alternate;
}
.paint.c {
  background: #6b4a30; /* umber: the grain between */
  top: 25%;
  inset-inline-start: 30%;
  width: 50%;
  opacity: 0.6;
  animation: drift-c 15s ease-in-out infinite alternate;
}
@keyframes drift-a {
  from { transform: translate(0, 0) scale(1); }
  to { transform: translate(-18%, 20%) scale(1.15); }
}
@keyframes drift-b {
  from { transform: translate(0, 0) scale(1.05); }
  to { transform: translate(22%, -16%) scale(0.92); }
}
@keyframes drift-c {
  from { transform: translate(-10%, 8%) scale(0.95); }
  to { transform: translate(12%, -14%) scale(1.2); }
}
/*
 * The window light: warm, low, from the start side, and slow — a sweep
 * every seven seconds rather than three, because light from a window does
 * not strobe. `screen` rather than `soft-light` so it lifts the wood to the
 * faded colour the prompt asks for instead of washing it white.
 */
.sweep {
  position: absolute;
  inset: -20% 0;
  width: 60%;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 214, 150, 0.18) 30%,
    rgba(255, 214, 150, 0.42) 50%,
    rgba(255, 214, 150, 0.18) 70%,
    transparent 100%
  );
  /* Blurred as well as graded: at the pool's edge a graded bar alone still
     read as a hard diagonal line, which is a reflection, not window light. */
  filter: blur(14px);
  transform: translateX(-160%) skewX(-14deg);
  animation: sweep 7s cubic-bezier(0.45, 0, 0.3, 1) infinite;
  mix-blend-mode: screen;
}
@keyframes sweep {
  0% { transform: translateX(-160%) skewX(-14deg); }
  70%, 100% { transform: translateX(340%) skewX(-14deg); }
}
/* The sweep crosses the way the light does: from the start side. */
:root[dir='rtl'] .sweep {
  animation-direction: reverse;
}
.paintcap {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: #fbf3e6;
  text-shadow: 0 1px 2px rgba(40, 24, 12, 0.5);
}
.paintspark {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(251, 243, 230, 0.86);
  color: #4a3120;
  box-shadow: 0 2px 4px rgba(40, 24, 12, 0.2), 0 8px 20px rgba(40, 24, 12, 0.25);
  animation: breathe 1.8s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.12); }
}
/* The import's slot has no card around it: give it the card's rounding. */
.importshot {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  margin-bottom: 14px;
}
@media (prefers-reduced-motion: reduce) {
  .paint,
  .sweep,
  .paintspark {
    animation: none;
  }
}

/* ================================================================ editor */

.editor {
  padding-bottom: calc(var(--safe-b) + 40px);
}

.flabel {
  display: block;
  margin: 16px 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Cuisine takes the room; the two numeric fields need only a few characters. */
.frow {
  display: grid;
  grid-template-columns: 1fr 5rem 5rem;
  gap: 10px;
  align-items: end;
}

.erows {
  display: grid;
  gap: 8px;
}

.erow {
  display: grid;
  grid-template-columns: 5.5rem 1fr 34px;
  gap: 8px;
  align-items: center;
}
.eqty {
  text-align: center;
  unicode-bidi: plaintext;
}

.estep {
  display: grid;
  grid-template-columns: 30px 1fr 34px;
  gap: 8px;
  align-items: start;
}
.estep .n {
  display: grid;
  place-items: center;
  width: 30px;
  height: 42px;
  color: var(--saffron);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.estep .rinput {
  min-height: 62px;
}

.erowdel {
  display: grid;
  place-items: center;
  width: 34px;
  height: 40px;
  border-radius: var(--r-sm);
  color: var(--muted);
}
.erowdel:active {
  color: var(--chilli);
}

/* ---------------------------------------- agent trace and proposals */

/* A step that failed reads as information, not alarm: the agent tried a page
   and could not read it, which is worth seeing and not worth shouting about. */
.trace .t.bad {
  color: var(--chilli);
}
.trace .t.bad svg {
  color: var(--chilli);
}
.trace .t.done svg {
  color: var(--saffron);
}

.otag {
  margin: 0 0 6px;
  font-size: 14px;
  font-weight: 550;
  color: var(--ink-soft);
}
.oblurb {
  margin: 0 0 8px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink-soft);
  unicode-bidi: plaintext;
}
.ometa {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
  unicode-bidi: plaintext;
}
/* What the agent decided and why — the thing that separates a proposal from a
   list of ingredients, so it gets the accent rail rather than plain body text. */
.osummary {
  margin-top: 11px;
  padding: 11px 13px;
  background: var(--sunk);
  border-radius: var(--r-sm);
  border-inline-start: 2px solid var(--saffron);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--ink-soft);
  unicode-bidi: plaintext;
}
.ocost {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  font-size: 12px;
  color: var(--muted);
}

.chat .empty svg {
  color: var(--saffron);
}

/* ================================================================ shared page */

/*
 * A shared recipe is read by someone who may have no account and no idea what
 * this app is. The badge says what they are looking at before they wonder.
 */
.shared {
  padding-bottom: 40px;
}
.sharedtop {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: calc(var(--safe-t) + 16px) 18px 12px;
}
.sharedbadge {
  padding: 4px 11px;
  border-radius: 999px;
  background: var(--saffron-wash);
  color: var(--saffron);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.03em;
}
.sharedby {
  font-size: 13px;
  color: var(--muted);
  unicode-bidi: plaintext;
}

/* The version picker on a shared page carries a date rather than a count. */
.chip .cnum {
  font-variant-numeric: normal;
}

.link.revoke {
  display: inline;
  padding: 0;
  font: inherit;
  color: var(--chilli);
  text-decoration: underline;
  background: none;
  border: 0;
}

/* The summary rewrite sits under the version card, quieter than the card. */
.summarybtn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 8px;
  font-size: 12.5px;
  padding: 5px 11px;
}

/* ================================================================ pictures */

/*
 * The hero, before there is one.
 *
 * An empty 4:3 block with nothing in it reads as a broken image, so the empty
 * state says what it is and offers the one action that fixes it. It keeps the
 * hero's own dimensions, which stops the whole screen jumping when a picture
 * finally arrives.
 */
.heroempty {
  position: absolute;
  inset: 0;
  /* Above the gradient, so the empty state is not tinted either. */
  z-index: 1;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
  padding: 20px;
  text-align: center;
  color: var(--muted);
  background: linear-gradient(160deg, var(--sunk), var(--card));
}
.heroempty b {
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
}
.heroempty p {
  margin: 0;
  max-width: 34ch;
  font-size: 13px;
  line-height: 1.5;
}

/* The price on a spending button. `direction: ltr` because it is a number with
   a unit and has no strong character of its own to resolve from (rule 17). */
.pricetag {
  flex: none;
  align-self: center;
  direction: ltr;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--saffron);
  background: var(--saffron-wash);
}

/* A `.bigopt` that is really a file picker: the input covers it invisibly, the
   same trick `.dropzone` uses, so the whole card is the tap target. */
label.bigopt {
  position: relative;
  cursor: pointer;
}
label.bigopt input[type='file'] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}
/* "Add the recipe's description" — a small action under a field, not a button
   that competes with the one that spends. */
.linkbtn {
  display: inline-block;
  margin-top: 6px;
  color: var(--saffron);
  font-size: 13px;
  font-weight: 600;
}
.linkbtn:disabled {
  opacity: 0.45;
}
.sheet header .headback h3 {
  margin: 0;
}

.bigopt:disabled,
.bigopt.disabled {
  opacity: 0.55;
  pointer-events: none;
}

.spin.big {
  width: 28px;
  height: 28px;
  border-width: 3px;
}

/* The recipe screen while its versions are still on the way. */
.loadingpad {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding-top: 34px;
  color: var(--muted);
  font-size: 14px;
}
.loadingpad .note,
.loadingpad .btn {
  justify-self: stretch;
}

/* ================================================================ the agent, thinking */

/*
 * One turn of the agent: its reasoning, what it did, what it said, and what it
 * proposed. A grid rather than stacked margins so the gaps between the parts
 * stay equal whichever of them are present — most turns have two of the five.
 */
.turn {
  justify-self: start;
  width: 100%;
  display: grid;
  gap: 10px;
}

/*
 * The reasoning: live, ONE line — the current step's title with the spinner
 * and the shimmer, replaced by the next when the pacing releases it; done, a
 * button that opens the whole transcript as a chain with a rail, a dot per
 * step, and each step's body under its title. The chain used to be the live
 * view too (every step staying on screen as it arrived); the owner asked for
 * one title at a time, with the bodies behind the button (14 Sep 2026).
 */
.thinking .thead {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  background: none;
  border: 0;
  padding: 0;
  text-align: start;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
}
.thinking .thead svg:last-child {
  transition: transform 0.2s var(--ease);
  transform: scaleX(var(--dir));
}
.thinking.open .thead svg:last-child {
  transform: scaleX(var(--dir)) rotate(90deg);
}

.tchain {
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
  display: grid;
  gap: 14px;
}
.thinking.open .tchain {
  margin-top: 12px;
}
/* The live slot: one step, no rail, a fixed height so the answer under it
   does not jump when a two-line title follows a one-line one. */
.thinking.live {
  min-height: 24px;
}
/* The rail. Drawn once, behind the dots, so it cannot develop gaps. */
.tchain::before {
  content: '';
  position: absolute;
  inset-block: 8px 6px;
  inset-inline-start: 7px;
  width: 2px;
  border-radius: 2px;
  background: var(--line-strong);
}
.thought {
  position: relative;
  padding-inline-start: 26px;
  display: grid;
  gap: 4px;
  animation: stepin 0.36s var(--ease) both;
}
.thought .tdot {
  position: absolute;
  inset-inline-start: 0;
  top: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--line-strong);
  display: grid;
  place-items: center;
}
/* A finished step's dot fills in; the newest one holds the spinner instead. */
.thought:not(.newest) .tdot::after {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--saffron);
}
.thought.newest .tdot {
  border-color: var(--saffron);
}
.thought .tdot .spin {
  width: 9px;
  height: 9px;
  border-width: 2px;
  border-top-color: var(--saffron);
}
.thought .ttitle {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink-soft);
  line-height: 1.4;
  /* The model writes its own headings, in the cook's language: content of
     unknown direction in a UI of known direction. */
  unicode-bidi: plaintext;
}
.thinking.live .thought:not(.newest) .ttitle {
  color: var(--muted);
}
.tbodytext {
  margin: 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
  unicode-bidi: plaintext;
}

@keyframes stepin {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
}

/*
 * The shimmer is the answer to "is it stuck?".
 *
 * A spinner says the app is alive; it does not say the ANSWER is alive, and the
 * gap between two reasoning steps can be five seconds. A slow sweep across the
 * words themselves keeps the thing the reader is looking at in motion.
 * `background-clip: text` paints the gradient through the glyphs, so it works on
 * Hebrew and English alike and needs no extra element.
 */
@keyframes shimmer {
  to {
    background-position: -200% 0;
  }
}
.thinking.live .ttitle.shimmer {
  background-image: linear-gradient(
    100deg,
    var(--ink-soft) 40%,
    var(--saffron) 50%,
    var(--ink-soft) 60%
  );
  background-size: 200% 100%;
  background-position: 0 0;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shimmer 2.4s linear infinite;
}
/* The sweep runs the way the words do. Below the rule above because
   `animation` is a shorthand and resets `animation-direction`. */
:root[dir='rtl'] .thinking.live .ttitle.shimmer {
  animation-direction: reverse;
}

@media (prefers-reduced-motion: reduce) {
  /* No sweep, no rise — and the colour comes back, or `color: transparent`
     leaves the heading invisible. */
  .thinking.live .ttitle.shimmer {
    background-image: none;
    -webkit-background-clip: border-box;
    background-clip: border-box;
    color: var(--ink-soft);
    animation: none;
  }
  .thought {
    animation: none;
  }
}

/* ================================================================ a question */

/*
 * The agent asking something.
 *
 * Bordered in saffron because it is the one thing in a transcript that is
 * WAITING on the reader — everything else can be scrolled past.
 */
.qcard {
  border: 1px solid var(--saffron);
  background: var(--saffron-wash);
  border-radius: var(--r-lg);
  padding: 14px 15px;
}
.qcard.answered {
  border-color: var(--line);
  background: var(--sunk);
}
.qtext {
  margin: 0 0 11px;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.5;
  unicode-bidi: plaintext;
}
.qopts {
  display: grid;
  gap: 8px;
}
.qopt {
  display: grid;
  gap: 2px;
  width: 100%;
  text-align: start;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 10px 13px;
  transition: border-color 0.15s var(--ease), transform 0.15s var(--ease);
}
.qopt:active {
  transform: scale(0.99);
}
.qopt b {
  font-size: 14px;
  font-weight: 600;
  unicode-bidi: plaintext;
}
.qopt span {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
  unicode-bidi: plaintext;
}
.qopt.picked {
  border-color: var(--saffron);
}
.qopt.other {
  border-style: dashed;
  background: none;
}
.qcard.answered .qopt:not(.picked) {
  opacity: 0.5;
}
/* ================================================================ the offer */

/*
 * The recipe it would write, and the one button that writes it. Bordered in
 * the accent like a question — it is the one other thing in a transcript
 * that waits on the reader — but on the card's paper, because what is in it
 * is the recipe's own name and line, a preview of the card to come. A past
 * offer (talked past, or read back from the thread) keeps the name and loses
 * the button and the border: a record, not a prompt.
 */
.offercard {
  border: 1px solid var(--saffron);
  background: var(--card);
  border-radius: var(--r-lg);
  padding: 14px 16px 15px;
  box-shadow: var(--lift-1);
  display: grid;
  gap: 6px;
}
.offercard.past {
  border-color: var(--line);
  box-shadow: none;
}
.offercard .oready {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--saffron);
}
.offercard.past .oready {
  color: var(--muted);
}
.offercard h4 {
  margin: 0;
  font-size: 21px;
  line-height: 1.2;
  unicode-bidi: plaintext;
}
.offercard .otag {
  margin: 0;
}
.offercard .btn {
  margin-top: 8px;
}
.offercard .ohint {
  margin: 2px 0 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--muted);
  text-align: center;
}

/* ================================================================ the preview */

.preview .oblurb {
  unicode-bidi: plaintext;
}
.previewtoggle {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 11px;
  background: none;
  border: 0;
  padding: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--saffron);
}
.previewtoggle svg {
  transition: transform 0.2s var(--ease);
}
.previewbody {
  margin-top: 12px;
  border-top: 1px solid var(--line);
  padding-top: 13px;
}
/*
 * ONE grid for the whole list, so every amount shares a column and every item
 * starts on the same line. The rows used to be independent flex lines with a
 * `min-width` on the amount — which aligns nothing, because "1½ cups" is wider
 * than the minimum and "5" is narrower, and each row sized itself. With
 * `display: contents` on the <li> the cells are grid children directly, and
 * `max-content` makes the amount column exactly as wide as the widest amount.
 */
.pring {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 12px;
  row-gap: 7px;
  font-size: 14px;
  line-height: 1.5;
}
.pring li {
  display: contents;
}
.pramt {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  unicode-bidi: plaintext;
  /* Amounts read best against a common edge, and in a column that is where the
     digits are. Numbers are neutral characters, so the alignment has to be
     stated rather than inherited. */
  text-align: end;
}
.pritem {
  unicode-bidi: plaintext;
}
/* Same qualifier styling as the recipe screen's list, which is a different
   element with a different parent — see the .ings .qnote rule above. */
.pring .qnote {
  color: var(--muted);
  font-size: 13px;
}
.prstep {
  margin: 0;
  padding-inline-start: 20px;
  display: grid;
  gap: 11px;
}
.prstep li {
  font-size: 14px;
  line-height: 1.6;
}
.prstep p {
  margin: 0;
  unicode-bidi: plaintext;
}
.prtip {
  margin-top: 4px !important;
  font-size: 13px;
  color: var(--muted);
}

/* ================================================================ suggestions */

/*
 * Ideas the agent thought worth mentioning. Visually quieter than the recipe on
 * purpose — they are extras, and a suggestion block that competes with the
 * answer reads as second-guessing rather than as help.
 */
.ideas {
  margin-top: 15px;
  border-top: 1px solid var(--line);
  padding-top: 13px;
  display: grid;
  gap: 10px;
}
/*
 * Sentence case, unlike every other `minihead`.
 *
 * The others are one- or two-word labels, where uppercase and wide tracking
 * read as a heading. This one is a sentence — it has to say that tapping does
 * something — and a tracked-out uppercase sentence reads as shouting and is
 * genuinely slower to parse, in Hebrew and Russian especially.
 */
.ideas .minihead {
  margin: 0;
  text-transform: none;
  letter-spacing: 0.01em;
  font-size: 12.5px;
}
.idea {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 2px 10px;
  width: 100%;
  text-align: start;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  padding: 8px 10px;
  margin-inline: -10px;
  transition: background 0.15s var(--ease), border-color 0.15s var(--ease);
}
.idea:hover,
.idea:active {
  background: var(--sunk);
  border-color: var(--line);
}
.idea b,
.idea span {
  grid-column: 1;
}
/* The chevron spans both rows and points the way the text runs. */
.idea svg {
  grid-column: 2;
  grid-row: 1 / -1;
  color: var(--saffron);
  transform: scaleX(var(--dir));
}
.idea b {
  font-size: 14px;
  font-weight: 600;
  unicode-bidi: plaintext;
}
.idea span {
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
  unicode-bidi: plaintext;
}

/* ================================================================ markdown */

.md > :first-child {
  margin-top: 0;
}
.md > :last-child {
  margin-bottom: 0;
}
.md h4 {
  margin: 14px 0 6px;
  font-size: 15px;
  font-weight: 600;
}
.md ul {
  margin: 0 0 9px;
  padding-inline-start: 20px;
  display: grid;
  gap: 5px;
}
.md code {
  font: 0.92em ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--sunk);
  border-radius: 5px;
  padding: 1px 5px;
}

/* The discussion sheet is a conversation now, so it needs the height for one. */
.sheet.tall {
  height: 88dvh;
  max-height: 88dvh;
}
.sheet.tall .body.msgs {
  flex: 1;
  min-height: 0;
  display: grid;
  align-content: start;
  gap: 14px;
}
.sheet.tall .composer {
  flex: none;
  /* The sheet is its own scroll container, so the composer sits at the bottom of
     THAT rather than sticking to the viewport above the tab bar. */
  position: static;
  bottom: auto;
  padding-bottom: 12px;
  background: var(--paper);
}

/*
 * "There is a newer version." At the bottom rather than at the top: it is an
 * offer, not an alarm, and it must not displace the header of whatever the
 * user is in the middle of reading.
 */
.updatebar {
  position: fixed;
  inset-inline: 12px;
  bottom: calc(var(--safe-b) + 12px);
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px 11px 16px;
  border-radius: var(--r-lg);
  background: var(--card);
  border: 1px solid var(--line-strong);
  box-shadow: var(--lift-3);
  animation: rise 0.3s var(--ease);
}
.updatebar span {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  line-height: 1.4;
}
@media (min-width: 721px) {
  .updatebar {
    max-width: 420px;
    margin-inline: auto;
  }
}

/* ================================================================ discussion openers */

/*
 * What the sheet is for, in the background.
 *
 * One muted paragraph under the last message — or alone, centred, when there
 * is no history yet — naming the three things people do here. Not a bubble:
 * it is the app's note, not a turn, and it goes the moment anything is sent.
 * It replaced four "door" buttons and a row of chips (15 Sep 2026); see the
 * comment in Discuss.tsx.
 */
.discusshint {
  margin: 0;
  padding: 6px 12px;
  justify-self: center;
  max-width: 34ch;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--muted);
  text-align: center;
  text-wrap: balance;
}
.sheet.tall .body.msgs.empty {
  align-content: center;
}

/* Two ways to keep a revision, and neither is the obvious default — one makes a
   version of this recipe, the other makes a recipe of its own. */
.keepbtns {
  display: grid;
  gap: 8px;
  margin-top: 13px;
}

/* The two real ways a recipe gets here, offered where the list would be. */
.emptyactions {
  display: grid;
  gap: 9px;
  margin-top: 18px;
  min-width: 220px;
}

/* The page an import ends on when the reading found nothing. The mark is
   red because it is an error (the palette's one meaning for `--chilli`),
   in a wash so it does not shout; the button is the only way on. */
.importfailed {
  padding-top: 48px;
}
.importfailed .failmark {
  display: inline-grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--chilli-wash);
  color: var(--chilli);
}
.importfailed p {
  max-width: 34em;
  margin-inline: auto;
}
.importfailed .failnote {
  margin-top: 10px;
  font-size: 13px;
}
.importfailed .emptyactions {
  justify-content: center;
  justify-items: center;
  margin-top: 22px;
}

/* A version or a correction — both legitimate, so neither is hidden. */
.savebtns {
  display: grid;
  gap: 9px;
}

/* ================================================================ the picture on a card */

/*
 * The hero area of a proposed recipe, above the title. 16:9 like the offer's
 * `.shot` used to be, so a card with a picture and one without are the same
 * width and the image slots in where the empty state was — no reflow when it
 * arrives.
 */
.oshot {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--sunk);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}
.oshot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  animation: fade 0.35s var(--ease);
}
/* "Try another", on top of the picture it would replace. */
.oshot .glassbtn.again {
  position: absolute;
  inset-block-end: 10px;
  inset-inline-end: 10px;
}
/*
 * A Commons photograph carries its credit ON the picture, small, where the
 * licence asks for it and where it does not fight the recipe underneath.
 */
.oshot .credit {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  max-inline-size: calc(100% - 56px);
  padding: 4px 10px;
  font-size: 10.5px;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.9);
  background: linear-gradient(to top, rgba(22, 13, 6, 0.7), rgba(22, 13, 6, 0));
  border-start-end-radius: 8px;
  unicode-bidi: plaintext;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/*
 * Not a broken image, not a grey rectangle: the one action that fills it, with
 * its price — the picture is the most expensive thing a tap can do here, and
 * rule 9 says a spend is quoted and opted into.
 */
.oshotempty {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 8px;
  width: 100%;
  background: linear-gradient(160deg, var(--sunk), var(--card));
  color: var(--muted);
  cursor: pointer;
}
.oshotempty b {
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.oshotempty:disabled {
  cursor: default;
}

/* ================================================================ pictures, in bulk */

/*
 * The offer to fill in every missing picture at once, above the grid. One row:
 * what is missing, the price for all of it, and the two answers. While the
 * batch runs the same row is the progress, so the eye does not have to find a
 * new place to look.
 */
.picbanner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 18px 14px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--card);
  box-shadow: var(--lift-1);
  color: var(--ink-soft);
}
.picbanner.stopped {
  border-color: var(--chilli);
}
.picbanner .pbtext {
  flex: 1;
  min-width: 12ch;
  font-size: 14px;
  line-height: 1.45;
}
.picbanner .btn {
  padding: 8px 12px;
  font-size: 13.5px;
}

/* A card with no picture says so, quietly. */
.noshot {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--line-strong);
}
