/* ==========================================================================
   The Recipe Box — layered on top of /css/style.css.
   Every colour, space, and radius below comes from a token defined there, so
   retuning the main site retunes this too. Add nothing here that could be a
   token instead.
   ========================================================================== */

.rb-wrap {
  width: 100%;
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 var(--space-6);
}

/* — header — */
.rb-header {
  border-bottom: 1px solid var(--color-divider);
  padding: var(--space-6) 0;
  margin-bottom: var(--rhythm);
}
.rb-header-inner {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
}
.rb-brand {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  font-size: 19px;
  color: var(--color-text);
  text-decoration: none;
  letter-spacing: -0.01em;
}
.rb-brand:hover { color: var(--color-accent); }
.rb-nav { display: flex; gap: var(--space-6); font-size: 14px; align-items: baseline; }
.rb-nav a { color: var(--color-neutral-400); text-decoration: none; }
.rb-nav a:hover { color: var(--color-accent-2); }

/* Who's signed in. The dot is the at-a-glance part — on a family box where
   five people share a laptop, "am I still Josie?" is a real question. */
.rb-whoami { display: inline-flex; align-items: baseline; gap: var(--space-3); }
.rb-whoami-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
  flex: none;
  transform: translateY(-1px);
}
.rb-whoami a { color: var(--color-neutral-400); }
.rb-whoami strong { color: var(--color-text); font-weight: 600; }
@media (max-width: 560px) {
  .rb-header-inner { flex-wrap: wrap; }
  .rb-nav { gap: var(--space-5); font-size: 13px; }
}

/* The footer is pinned to the bottom of the screen, the way the rest of the
   site pins its link bar — same fixed position, same grain, same rule and
   shadow, so the two read as one site.

   A first attempt made the page a full-height column instead, which put the
   footer on the bottom edge of a SHORT page and at the end of a long one.
   That is the textbook sticky footer and it was not what was asked for: on a
   box with a hundred recipes the footer is a hundred recipes away, which is
   the same as not being there.

   The page reserves the bar's height below itself so nothing is ever hidden
   behind it — the same trick, and the same reason, as body's padding for the
   site link bar. --rb-footbar is the one number: the bar's height and the
   room kept for it cannot drift apart if they are the same value. It holds
   only if the bar stays one line, which is what the narrow rules below are
   for. */
.rb-page {
  --rb-footbar: 46px;
  /* The bar's height plus a gap. Reserving the height alone left the last
     line of the page touching the rule, which reads as cut off even though
     nothing is hidden. */
  padding-bottom: calc(var(--rb-footbar) + var(--space-6) + env(safe-area-inset-bottom, 0px));
}

.rb-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  box-sizing: border-box;
  height: calc(var(--rb-footbar) + env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  margin: 0;
  /* Room for the iPhone home indicator, like the site bar keeps. */
  padding: 0 0 env(safe-area-inset-bottom, 0px);
  background-color: var(--color-bg);
  background-image: var(--grain-paper);
  border-top: 2px solid var(--color-rule);
  box-shadow: 0 -8px 24px rgba(50, 35, 15, 0.18);
  color: var(--color-text-quiet);
  font-size: 13px;
}
.rb-footer-inner {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  width: 100%;
}
/* Neither half may push the bar to two lines, which is what the reserved
   height is promised against. */
.rb-footer-inner p { margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rb-version { flex: none; }

/* On a phone the sentence is the part worth losing: it says nothing you
   cannot see by looking at the page. */
@media (max-width: 680px) {
  .rb-footer-long { display: none; }
}
@media (max-width: 560px) {
  .rb-version-when { display: none; }
}
@media (max-width: 380px) {
  .rb-page { --rb-footbar: 42px; }
  .rb-footer { font-size: 12px; }
}

.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: var(--space-4);
  top: var(--space-4);
  z-index: 10;
  background: var(--color-surface);
  color: var(--color-text);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
}

/* — search + filters — */
.rb-search {
  display: flex;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  flex-wrap: wrap;
}

/* Keep the search against the top of the screen while a long list scrolls
   under it, on the pages that ARE a list. A recipe does not get this: there
   you are reading, and a bar over the method is in the way.

   The negative top margin and matching padding are what stop the row from
   showing a sliver of scrolling list above itself as it sticks — the sticky
   box has to reach the top edge, so it is grown upward by the gap it would
   otherwise leave. The background is not optional either: a sticky element
   without one is transparent, and the list scrolls through the text. */
.rb-sticky-search .rb-search {
  position: sticky;
  top: 0;
  z-index: 5;
  margin-top: calc(var(--space-5) * -1);
  padding: var(--space-5) 0 var(--space-4);
  /* The page is parchment WITH a grain over it, so a flat fill of the same
     colour is not the same thing — it reads as a faint clean band across an
     otherwise textured page. Lay the grain on the bar as well. */
  background-color: var(--color-bg);
  background-image: var(--grain-paper);
  border-bottom: 1px solid var(--color-divider);
  margin-bottom: var(--space-5);
}

/* Nothing to stick to on a short screen turned sideways: a keyboard leaves
   so little height that the bar would be most of it. */
@media (max-height: 460px) {
  .rb-sticky-search .rb-search { position: static; }
}

/* A sticky element cannot escape an ancestor that clips, and it has nothing
   to stick within if the page itself is the scroller and some parent has
   been given a scroll box. Neither is true here, and this comment is so the
   next person who adds overflow to .rb-wrap knows what it breaks. */
.rb-search input[type="search"] {
  flex: 1 1 260px;
  min-width: 0;
}

.rb-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--rhythm);
}
.rb-chip {
  display: inline-block;
  padding: var(--space-2) var(--space-5);
  border-radius: 999px;
  border: 1px solid var(--color-neutral-800);
  background: transparent;
  color: var(--color-neutral-400);
  font-size: 13px;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color 120ms ease, color 120ms ease;
}
.rb-chip:hover {
  border-color: var(--color-accent-700);
  color: var(--color-accent-2);
}
.rb-chip[aria-current="true"] {
  border-color: var(--color-accent);
  background: var(--color-accent-900);
  color: var(--color-accent-200);
}
.rb-chip-count { color: var(--color-text-quiet); margin-left: var(--space-2); }

/* — grid — */
.rb-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: var(--space-6);
}

.rb-card {
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--shadow-sm);
  transition: transform 140ms ease, box-shadow 140ms ease;
}
.rb-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
.rb-card-img {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--color-neutral-900);
}
.rb-card-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.rb-card-img-empty {
  display: grid;
  place-items: center;
  color: var(--color-accent-800);
  font-family: var(--font-heading);
  font-size: 44px;
}

/* Document-scan card images: anchor the crop to the top of the page, where
   the recipe title is, instead of a centered slab of body text. */
.rb-card-img-scan { background: #fff; }
.rb-card-img-scan img { object-position: top; }

/* Colorful tiles for recipes with no images at all. One deep two-tone
   gradient per course, with the course emoji as the art. */
.rb-card-deco { display: grid; place-items: center; }
.rb-deco-emoji {
  font-size: 54px;
  line-height: 1;
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.35));
  transition: transform 140ms ease;
}
.rb-card:hover .rb-deco-emoji { transform: scale(1.12); }
.rb-deco-main      { background: linear-gradient(135deg, #4a2530, #2b1722); }
.rb-deco-side      { background: linear-gradient(135deg, #2c4237, #182a22); }
.rb-deco-appetizer { background: linear-gradient(135deg, #2c3352, #1a1f33); }
.rb-deco-soup      { background: linear-gradient(135deg, #4a3222, #2b1d14); }
.rb-deco-salad     { background: linear-gradient(135deg, #24422c, #152a1c); }
.rb-deco-bread     { background: linear-gradient(135deg, #453222, #291d14); }
.rb-deco-dessert   { background: linear-gradient(135deg, #472a44, #2a1a33); }
.rb-deco-breakfast { background: linear-gradient(135deg, #4a3a1e, #2c2214); }
.rb-deco-drink     { background: linear-gradient(135deg, #1e4147, #12282e); }
.rb-deco-sauce     { background: linear-gradient(135deg, #4a2a1e, #2b1812); }
.rb-deco-snack     { background: linear-gradient(135deg, #3d2b4a, #241a2e); }
.rb-deco-reference { background: linear-gradient(135deg, #333a45, #1e232c); }
.rb-deco-other     { background: linear-gradient(135deg, #33333a, #1f1f26); }
@media (prefers-reduced-motion: reduce) {
  .rb-deco-emoji, .rb-card:hover .rb-deco-emoji { transition: none; transform: none; }
}
.rb-card-body { padding: var(--space-5) var(--space-6) var(--space-6); }
.rb-card-body h3 {
  margin: 0 0 var(--space-3);
  font-size: 16px;
  font-weight: var(--font-heading-weight);
  line-height: 1.3;
}
.rb-card-desc {
  margin: 0 0 var(--space-4);
  font-size: 13px;
  color: var(--color-text-muted);
  line-height: 1.45;
}
.rb-card-meta { margin: 0; display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.rb-card-num {
  margin-left: auto;
  color: var(--color-text-quiet);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Secondary filter rows sit quieter than the course row. */
.rb-filters-secondary .rb-chip { font-size: 12px; padding: 1px var(--space-4); }

.rb-tagbrowse { margin-bottom: var(--rhythm); }
.rb-tagbrowse summary {
  cursor: pointer;
  font-size: 13px;
  color: var(--color-text-muted);
  margin-bottom: var(--space-4);
  user-select: none;
}
.rb-tagbrowse summary:hover { color: var(--color-accent-2); }
.rb-tagbrowse[open] summary { color: var(--color-neutral-400); }

.tag {
  display: inline-block;
  padding: 1px var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--color-neutral-900);
  color: var(--color-text-muted);
  font-size: 12px;
  letter-spacing: 0.02em;
}
/* accent-300 on accent-900 came to 3.7:1; accent-100 on the same tint is 6.7. */
.tag-family { background: var(--color-accent-900); color: var(--color-accent-100); }
.tag-draft  { background: #4a3a1c; color: #f0d69a; }

/* — single recipe — */
.rb-recipe { max-width: 760px; }
.rb-recipe h1 {
  font-size: 32px;
  line-height: 1.15;
  margin: 0 0 var(--space-4);
  letter-spacing: -0.02em;
}
.rb-lede {
  font-size: 16px;
  color: var(--color-neutral-400);
  margin: 0 0 var(--space-6);
}
.rb-meta {
  display: flex;
  gap: var(--space-8);
  flex-wrap: wrap;
  padding: var(--space-5) 0;
  border-top: 1px solid var(--color-divider);
  border-bottom: 1px solid var(--color-divider);
  margin-bottom: var(--rhythm);
  font-size: 13px;
}
.rb-meta dt {
  color: var(--color-text-quiet);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 2px;
}
.rb-meta dd { margin: 0; color: var(--color-text); }

.rb-columns {
  display: grid;
  grid-template-columns: minmax(200px, 1fr) 1.6fr;
  gap: var(--rhythm);
  align-items: start;
}
@media (max-width: 700px) {
  .rb-columns { grid-template-columns: 1fr; }
}

.rb-section-title {
  font-family: var(--font-heading);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-5);
}

/* Checkable ingredients — the whole point of reading this on a phone with
   flour on your hands. State is intentionally not persisted; it resets on
   reload, which is what you want next time you cook it. */
.rb-ingredients { list-style: none; margin: 0; padding: 0; }
.rb-ingredients li { margin-bottom: var(--space-4); line-height: 1.4; }
.rb-ingredients label {
  display: flex;
  gap: var(--space-4);
  align-items: baseline;
  cursor: pointer;
  font-size: 14px;
}
.rb-ingredients input { margin: 0; accent-color: var(--color-accent); flex: none; }
.rb-ingredients input:checked + span {
  text-decoration: line-through;
  color: var(--color-text-quiet);
}

.rb-steps { margin: 0; padding-left: 0; list-style: none; counter-reset: step; }
.rb-steps li {
  counter-increment: step;
  position: relative;
  padding-left: calc(var(--space-8) + var(--space-4));
  margin-bottom: var(--space-6);
  line-height: 1.6;
}
.rb-steps li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 0;
  width: var(--space-8);
  height: var(--space-8);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--color-accent-900);
  /* On its own accent tint, not on the page: accent-200 there is 4.4:1. */
  color: var(--color-accent-100);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.rb-notes {
  background: var(--color-surface);
  border-left: 2px solid var(--color-accent-700);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: var(--space-6);
  margin-top: var(--rhythm);
  font-size: 14px;
  white-space: pre-wrap;
}

.rb-scan { margin-top: var(--rhythm); }
.rb-scan img {
  width: 100%;
  border-radius: var(--radius-md);
  display: block;
  box-shadow: var(--shadow-sm);
}
.rb-scan figcaption {
  font-size: 12px;
  color: var(--color-text-quiet);
  margin-top: var(--space-3);
}

/* — "how do I add a recipe" — the four import boxes are alternatives, and
     nothing about four stacked panels said so. The numbered badge and the
     "or" between them carry that meaning without a paragraph of explaining. */
.rb-howto {
  background: var(--color-accent-900);
  border: 1px solid var(--color-accent-700);
  border-radius: var(--radius-lg);
  padding: var(--space-6) var(--space-8);
  margin-bottom: var(--rhythm);
  /* Same tint as an info note, so the same reasoning: measured against the
     accent-900 panel rather than the page, accent-200 is 4.4:1 and accent-100
     is 6.7. */
  color: var(--color-accent-100);
}
.rb-howto h2 { margin: 0 0 var(--space-3); font-size: 17px; }
.rb-howto a:not(.rb-btn) { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.rb-howto p { margin: 0 0 var(--space-5); font-size: 14px; }
.rb-howto-steps { margin: 0; padding-left: var(--space-7); font-size: 14px; }
.rb-howto-steps li { margin-bottom: var(--space-3); line-height: 1.5; }
.rb-howto strong { color: var(--color-text); }

.rb-route { position: relative; }
.rb-route-num {
  margin: 0 0 var(--space-3);
  font-family: var(--font-heading);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent-2);
}
.rb-or {
  text-align: center;
  margin: calc(var(--rhythm) * -0.55) 0 var(--rhythm);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-text-quiet);
}
.rb-next-hint {
  margin: var(--space-5) 0 0;
  font-size: 12px;
  color: var(--color-text-quiet);
}
.rb-next-hint::before { content: "→ "; }

/* — a job in progress —

   The old status line was a spinner, a sentence and a clock in one
   non-wrapping inline-flex row. On a phone the sentence was squeezed to two
   lines between the two, and the seconds ended up pinned against the right
   edge of the screen with nothing beside them. This is a block instead: the
   indicator keeps its own column, the words get the rest of the width, and
   both wrap like normal text when there isn't enough of it. */
.rb-working {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-5) var(--space-6);
  border-radius: var(--radius-md);
  background: var(--color-accent-900);
  border: 1px solid var(--color-accent-700);
  margin: var(--space-5) 0;
}
.rb-working[hidden] { display: none; }
.rb-working-body { min-width: 0; }      /* wrap the text rather than overflow */
.rb-working-what {
  margin: 0;
  font-size: 14px;
  color: var(--color-accent-100);
}
.rb-working-sub {
  margin: var(--space-2) 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--color-accent-300);
}
.rb-elapsed {
  font-variant-numeric: tabular-nums;
  color: var(--color-text-quiet);
}

/* — progress ring —

   One component, two honest states. It fills when there is a real number
   behind it — files banked out of files found, bytes sent out of bytes to
   send — and sweeps when there isn't. Transcription is the second kind: the
   API reports nothing at all until it returns, and a ring creeping towards
   100% on a timer would be a guess wearing the clothes of a measurement. The
   label follows the same rule, carrying the percentage where one exists and
   the elapsed clock where it doesn't, so the middle of the ring is never
   telling you something the page does not know. */
.rb-ring {
  --rb-ring-size: 52px;
  position: relative;
  flex: none;
  width: var(--rb-ring-size);
  height: var(--rb-ring-size);
}
.rb-ring svg {
  display: block;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);          /* start the arc at twelve o'clock */
}
.rb-ring circle { fill: none; stroke-width: 4; }
.rb-ring-track { stroke: var(--color-accent-800); }
.rb-ring-fill {
  stroke: var(--color-accent);
  stroke-linecap: round;
  transition: stroke-dashoffset 350ms ease;
}
/* The slice the in-flight item will fill once it lands. Dimmer than the fill
   and deliberately so: it is what is being worked on, not what is banked. */
.rb-ring-pending {
  stroke: var(--color-accent-700);
  transition: stroke-dashoffset 350ms ease;
}
.rb-ring.is-working .rb-ring-pending {
  animation: rb-ring-pulse 1.1s ease-in-out infinite alternate;
}
@keyframes rb-ring-pulse { from { opacity: 0.3; } to { opacity: 1; } }

.rb-ring-label {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-300);
}

/* Nothing to count: sweep a short arc instead of filling a long one. */
.rb-ring.is-waiting svg { animation: rb-ring-spin 1.15s linear infinite; }
.rb-ring.is-waiting .rb-ring-fill { transition: none; }
@keyframes rb-ring-spin {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(270deg); }
}

.rb-working-lite { border: 0; background: none; padding: 0; }

@media (max-width: 460px) {
  .rb-ring { --rb-ring-size: 44px; }
  .rb-working { gap: var(--space-4); padding: var(--space-4) var(--space-5); }
}
.rb-import-form.is-working,
#scan-form.is-working { opacity: 0.75; }
.rb-import-form.is-working input,
.rb-import-form.is-working textarea,
#scan-form.is-working input { pointer-events: none; }

/* — a field the duplicate check is pointing at — */
.rb-field-dup {
  border-left: 3px solid #d05a5a;
  padding-left: var(--space-5);
  margin-left: calc(var(--space-5) * -1);
}
.rb-field-dup label { color: #f0b8b8; }
.rb-field-dup input,
.rb-field-dup textarea {
  border-color: #d05a5a;
  background: #2a1818;
}
.rb-field-dup input:focus,
.rb-field-dup textarea:focus {
  border-color: #e07575;
  outline-color: #e07575;
}
.rb-dup-flag {
  margin: 0 0 var(--space-3);
  font-size: 12px;
  color: #f0b8b8;
}

/* — clickable column headings — */
.rb-table-sortable th { padding: 0; }
.rb-sort {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.rb-sort:hover { color: var(--color-accent-2); background: var(--color-surface); }
.rb-sort.is-sorted { color: var(--color-accent); }
.rb-sort-arrow { font-size: 11px; line-height: 1; }
/* Every sortable heading carries a mark, not just the one in use — otherwise
   nothing tells you the other columns can be clicked at all. The idle mark is
   faint and comes up on hover, so the affordance is there without competing
   with the arrow that shows the order actually applied. */
.rb-sort-arrow.is-idle { color: var(--color-text-quiet); }
.rb-sort:hover .rb-sort-arrow.is-idle { color: var(--color-text); }

/* — how much is actually in the box — */
.rb-counts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3) var(--space-5);
  margin: 0 0 var(--space-5);
  font-size: 14px;
  color: var(--color-muted);
}
.rb-counts b { color: var(--color-text); font-variant-numeric: tabular-nums; }
/* The counts are also filters; keep them looking like counts until hovered. */
.rb-counts a { color: inherit; text-decoration: none; }
.rb-counts a:hover { color: var(--color-text); text-decoration: underline; }
.rb-counts a[aria-current="true"] { color: var(--color-text); font-weight: 600; }
.rb-count-dot {
  display: inline-block;
  width: .5em;
  height: .5em;
  border-radius: 50%;
  margin-right: .4em;
}
.rb-count-live  .rb-count-dot { background: var(--color-accent); }
.rb-count-draft .rb-count-dot { background: var(--color-muted); }

/* — the edit form's header: which recipe am I looking at — */
.rb-edit-head { margin-bottom: var(--rhythm); }
.rb-edit-title {
  font-size: 24px;
  line-height: 1.2;
  margin: 0 0 var(--space-3);
  letter-spacing: -0.01em;
}
.rb-edit-num {
  font-family: var(--font-heading);
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  background: var(--color-surface);
  border-radius: var(--radius-sm, 4px);
  padding: 3px 7px;
  margin-right: var(--space-4);
  vertical-align: middle;
  white-space: nowrap;
}
.rb-edit-sub {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-quiet);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* — forms / admin — */
.rb-panel {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: var(--space-8);
  box-shadow: var(--shadow-sm);
  margin-bottom: var(--rhythm);
}
.rb-panel h2 { margin-top: 0; font-size: 17px; }

.rb-field { margin-bottom: var(--space-6); }
.rb-field label {
  display: block;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--color-neutral-400);
  margin-bottom: var(--space-3);
}
/* A hint: the small print under a control, a heading, or a panel.
   This was only ever defined as `.rb-field .rb-hint`, so the four dozen
   standalone ones — every "Way N" description on Add, the notes on Split
   scans, Bulk import and Duplicates — matched nothing at all and rendered as
   full-size body text in full-strength white, competing with the copy they
   were meant to sit quietly beneath. Eight of them carried an inline negative
   top margin to claw back the space that mis-sizing cost, which on a narrow
   screen dragged the paragraph up into the heading above it. Styling the class
   itself fixes every one and makes those hacks deletable. */
.rb-hint {
  font-size: 13px;
  color: var(--color-text-muted);
  line-height: 1.45;
}
p.rb-hint { margin: 0 0 var(--space-5); }

.rb-field .rb-hint {
  font-size: 12px;
  color: var(--color-text-quiet);
  margin-top: var(--space-2);
}
.rb-input,
input[type="text"], input[type="url"], input[type="search"],
input[type="password"], input[type="number"], textarea, select {
  width: 100%;
  padding: var(--space-4) var(--space-5);
  background: var(--color-bg);
  border: 1px solid var(--color-neutral-800);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
}
textarea { min-height: 140px; resize: vertical; line-height: 1.5; }
input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-900);
}
.rb-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-6);
}

.rb-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-md);
  border: 1px solid var(--color-neutral-700);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
  text-decoration: none;
}
.rb-btn:hover { border-color: var(--color-neutral-500); }
.rb-btn-primary {
  background: var(--color-accent-2);
  border-color: var(--color-accent-2);
  color: var(--color-bg);
  font-weight: 500;
}
.rb-btn-primary:hover { background: var(--color-accent-2); border-color: var(--color-accent-2); }
.rb-btn-danger { border-color: #8c2f2f; color: #8c2f2f; }
.rb-btn-danger:hover { border-color: #6d2222; color: #6d2222; background: rgba(140, 47, 47, 0.07); }
.rb-btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* A filled button at 45% opacity is a pale lavender slab with dark text on it —
   the least readable thing on screen at exactly the moment it is describing
   what the page is doing. Disabled drops the fill instead of fading it. */
.rb-btn-primary:disabled {
  opacity: 1;
  background: transparent;
  border-color: var(--color-accent-700);
  color: var(--color-accent-2);
}
.rb-actions { display: flex; gap: var(--space-4); flex-wrap: wrap; align-items: center; }

.rb-note {
  padding: var(--space-5) var(--space-6);
  border-radius: var(--radius-md);
  font-size: 13px;
  margin-bottom: var(--space-6);
  line-height: 1.5;
}
.rb-note-error { background: #f6dcd8; color: #7c2d22; border: 1px solid #d9a99f; }
.rb-note-ok    { background: #dde9d8; color: #35603a; border: 1px solid #a9c4a4; }
.rb-note-warn  { background: #f3e6c6; color: #6d5410; border: 1px solid #d2bd86; }
.rb-note-info  { background: var(--color-accent-900); color: var(--color-accent-100); border: 1px solid var(--color-accent-700); }
.rb-note ul { margin: var(--space-3) 0 0; padding-left: var(--space-6); }

/* A link inside a note was set in the accent colour with no underline, on a
   background built from the same accent — so it read as prose and the only way
   to find it was to drag the mouse across the paragraph. Underline it. Buttons
   are excluded: they already look like something to press. */
.rb-note a:not(.rb-btn) { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.rb-note-action { margin: var(--space-5) 0 0; }

.rb-empty {
  text-align: center;
  padding: calc(var(--rhythm) * 2) var(--space-6);
  color: var(--color-text-muted);
}

.rb-tabs {
  display: flex;
  gap: var(--space-2);
  border-bottom: 1px solid var(--color-divider);
  margin-bottom: var(--rhythm);
  /* A dozen tabs do not fit a phone, and left alone the row set the width of
     the page so every admin screen scrolled sideways, table and all.
     Scrolling the row instead fixed that and brought its own problem: a
     scrollbar track sitting under the tabs, plus a second one down the side,
     because setting overflow-x alone makes overflow-y compute to auto. Two
     sliders through the middle of the page chrome.
     Wrapping is the better answer for a nav. Nothing is hidden behind a
     scroll, there is no slider, and a narrow screen gets a second row of
     tabs — which is what a row of links should do when it runs out of room. */
  flex-wrap: wrap;
  row-gap: 0;
}
.rb-tab { white-space: nowrap; flex: none; }
.rb-tab {
  padding: var(--space-4) var(--space-6);
  color: var(--color-text-muted);
  text-decoration: none;
  font-size: 14px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.rb-tab[aria-current="page"] {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}

.rb-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.rb-table th {
  text-align: left;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-quiet);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-divider);
  font-weight: 400;
}
.rb-table td {
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-divider);
  vertical-align: middle;
}
.rb-table tr:hover td { background: var(--color-neutral-900); }
.rb-table a { color: var(--color-text); text-decoration: none; }
.rb-table a:hover { color: var(--color-accent-2); }

.rb-dropzone {
  border: 2px dashed var(--color-neutral-800);
  border-radius: var(--radius-lg);
  padding: var(--rhythm);
  text-align: center;
  color: var(--color-text-muted);
  transition: border-color 120ms ease, background 120ms ease;
}
.rb-dropzone.is-over {
  border-color: var(--color-accent);
  background: var(--color-accent-900);
  color: var(--color-accent-200);
}
.rb-dropzone input[type="file"] { display: block; margin: var(--space-5) auto 0; width: auto; }

.rb-preview { max-width: 100%; border-radius: var(--radius-md); margin-top: var(--space-5); }

/* Queued pages before an import. Numbered, because with several pages the
   pick order is the read order. */
.rb-previews {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin-top: var(--space-5);
}
.rb-preview-item { margin: 0; width: 120px; }
.rb-preview-item img {
  width: 120px;
  height: 120px;
  object-fit: cover;
  border-radius: var(--radius-md);
  display: block;
}
.rb-preview-pdf {
  width: 120px;
  height: 120px;
  border-radius: var(--radius-md);
  border: 1px dashed var(--color-divider);
  display: grid;
  place-items: center;
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
}
.rb-preview-item figcaption {
  font-size: 11px;
  color: var(--color-text-quiet);
  margin-top: var(--space-3);
  overflow-wrap: anywhere;
}

.rb-spinner {
  display: inline-block;
  width: 13px; height: 13px;
  border: 2px solid var(--color-accent-800);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: rb-spin 700ms linear infinite;
}
@keyframes rb-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .rb-spinner { animation-duration: 2s; }
  .rb-ring.is-waiting svg { animation-duration: 3s; }
  .rb-ring.is-working .rb-ring-pending { animation: none; opacity: 0.8; }
  .rb-ring-fill, .rb-ring-pending { transition: none; }
  .rb-card { transition: none; }
  .rb-card:hover { transform: none; }
}

/* The Print button, and the line that only exists on paper. */
.rb-recipe-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-6);
}
.rb-recipe-head h1 { flex: 1; }
.rb-print-btn { flex: none; }
/* The Edit link next to it: same shape, same disappearing act on paper. */
.rb-edit-btn { flex: none; }
.rb-print-only { display: none; }

/* — Notes and likes — */

/* The honeypot. Off-screen rather than display:none, because a bot that reads
   the stylesheet skips what is plainly hidden and fills in what is not; this
   is invisible to a person and ordinary to a parser. */
.rb-hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.rb-like-form { flex: none; margin: 0; }
.rb-like-btn { gap: var(--space-3); }
.rb-like-btn.is-mine { border-color: var(--color-accent); color: var(--color-accent); }
.rb-like-count { font-variant-numeric: tabular-nums; }

.rb-say { margin-top: var(--rhythm); }
.rb-say-quiet { border-top: 1px dotted var(--rule); padding-top: var(--space-6); }
.rb-say-quiet summary { cursor: pointer; color: var(--color-text-muted); font-size: 14px; }

.rb-say-list { list-style: none; padding: 0; margin: 0 0 var(--rhythm); }
.rb-say-list li {
  padding: var(--space-5) 0;
  border-bottom: 1px dotted var(--rule);
}
.rb-say-list li:last-child { border-bottom: 0; }
.rb-say-body { margin: 0; white-space: pre-wrap; }
.rb-say-meta { margin: var(--space-3) 0 0; font-size: 13px; color: var(--color-text-quiet); }
.rb-say-inbox .rb-say-meta { margin: 0 0 var(--space-3); opacity: 1; }
.rb-say-form { max-width: 46rem; }

/* A conversation is not part of the recipe, and printing one wastes paper. */
@media print { .rb-say, .rb-like-form { display: none !important; } }

/* Printing a recipe should give you the recipe, not the website — and it
   should fit on ONE sheet. Everything below is in service of that: the
   chrome goes, photos go, type shrinks to 10pt, the generous screen rhythm
   collapses, and page-break rules stop a step list from being guillotined
   across sheets when a long recipe genuinely needs a second page. */
@media print {
  @page { size: letter portrait; margin: 12mm 14mm; }

  .rb-header, .rb-footer, .skip-link,
  .rb-scan, .rb-hero, .rb-note, .rb-filters, .rb-print-btn, .rb-edit-btn,
  .rb-tabs, .rb-search { display: none !important; }

  .rb-print-only { display: block; }

  html, body {
    background: #fff;
    color: #000;
    font-size: 10pt;
    line-height: 1.35;
  }
  /* The screen keeps a strip clear at the foot for the pinned bar. On paper
     there is no bar, and the whole point of these rules is one sheet. */
  .rb-page { padding-bottom: 0; }
  .rb-wrap { max-width: none; padding: 0; margin: 0; }
  main.rb-wrap { margin: 0; }
  .rb-recipe { max-width: none; }

  .rb-recipe h1 { font-size: 19pt; margin: 0 0 2mm; }
  .rb-print-source {
    font-size: 8pt;
    color: #555;
    margin: 0 0 3mm;
    padding-bottom: 2mm;
    border-bottom: 1px solid #bbb;
  }
  .rb-lede { font-size: 10pt; color: #333; margin: 0 0 3mm; }

  .rb-meta {
    gap: 6mm;
    padding: 2mm 0;
    margin-bottom: 4mm;
    font-size: 8.5pt;
    border-color: #bbb;
  }
  .rb-meta dt { color: #666; font-size: 7pt; }

  /* Ingredients narrow, method wide — the shape that fits a page. */
  .rb-columns { display: grid; grid-template-columns: 1fr 1.9fr; gap: 8mm; }
  .rb-section-title { font-size: 8pt; color: #444; margin: 0 0 2mm; }

  .rb-ingredients li { margin-bottom: 1.2mm; }
  .rb-ingredients label { font-size: 9.5pt; gap: 1.5mm; }
  .rb-ingredients input { display: none; }   /* tick boxes are a screen idea */

  .rb-steps li {
    margin-bottom: 2.2mm;
    line-height: 1.35;
    padding-left: 7mm;
    page-break-inside: avoid;
    break-inside: avoid;
  }
  .rb-steps li::before {
    background: #fff;
    color: #000;
    border: 1px solid #666;
    width: 4.5mm;
    height: 4.5mm;
    font-size: 7.5pt;
  }

  .rb-notes {
    border-left: 2px solid #999;
    background: #f4f4f4;
    padding: 2.5mm 3mm;
    margin-top: 4mm;
    font-size: 9pt;
    page-break-inside: avoid;
  }

  a { color: #000; text-decoration: none; }
}

/* Hero photo on the recipe page — the finished dish, pulled from the source
   site at import (or by the admin image backfill). Capped height so a tall
   source photo doesn't push the ingredients below the fold. */
.rb-hero {
  margin: var(--rhythm) 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.rb-hero img {
  width: 100%;
  max-height: 420px;
  object-fit: cover;
  display: block;
}

/* Split-scans report: a plain list of what came out and what was left behind.
   The hint sits under the link on a narrow screen and beside it on a wide one,
   which is why it is a block rather than an inline span. */
.rb-split-list { margin: 0; padding-left: var(--space-7); font-size: 14px; }
.rb-split-list li { margin-bottom: var(--space-3); line-height: 1.5; }
.rb-split-list .rb-hint { display: block; }

/* Duplicates: one group per block, each row a recipe with its provenance and a
   way to delete it. The delete button sits at the end of the row rather than
   under it, so a long group stays scannable as a list of titles. */
.rb-dup-group {
  list-style: none;
  margin: 0 0 var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--rule);
  border-radius: 8px;
}
.rb-dup-group:last-child { margin-bottom: 0; }
/* Deleting returns to this group by anchor. Without the margin the browser
   parks it flush against the top edge, which reads as "the page jumped"
   rather than "here is where you were". */
.rb-dup-group { scroll-margin-top: var(--space-6); }
.rb-dup-item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  font-size: 15px;
}
.rb-dup-item + .rb-dup-item { border-top: 1px dotted var(--rule); }
.rb-dup-meta { font-size: 13px; color: var(--color-text-quiet); }
.rb-dup-delete { margin-left: auto; }
.rb-dup-score { font-size: 13px; color: var(--color-text-quiet); margin-bottom: var(--space-2); }
.rb-dup-keep { list-style: none; margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px dotted var(--rule); }

/* — Review queue —
   A hundred rows that are mostly fine, with a handful that are not. The
   flagged ones are what the page is for, so they get the colour and the
   left edge; everything else stays quiet enough to scroll past. */
.rb-review-tally { display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-5); align-items: baseline; }
.rb-review-tally span::before { content: '·'; margin-right: var(--space-4); opacity: .5; }

.rb-review-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}
.rb-review-bar-end { margin: var(--space-5) 0 0; }

.rb-review-table td { vertical-align: top; }
.rb-review-tick { width: 1%; padding-right: 0; }
.rb-review-tick input { width: 17px; height: 17px; cursor: pointer; }
.rb-review-title { display: block; cursor: pointer; }
.rb-review-meta { display: block; font-size: 13px; color: var(--color-text-quiet); margin-top: 2px; }

/* The flagged rows are the ones worth stopping at; a bar down the edge
   survives a fast scroll where a word in the third column does not. */
.rb-review-table tr.is-flagged td:first-child { box-shadow: inset 3px 0 0 #c0705f; }

.rb-flag {
  display: inline-block;
  font-size: 12px;
  line-height: 1.4;
  padding: 1px 7px;
  border-radius: 999px;
  margin: 0 var(--space-3) 2px 0;
}
.rb-flag-stop { background: #f6dcd8; color: #7c2d22; border: 1px solid #d9a99f; }
.rb-flag-soft { background: #f3e6c6; color: #6d5410; border: 1px solid #d2bd86; }
.rb-flag-ok   { background: #dde9d8; color: #35603a; border: 1px solid #a9c4a4; }

/* — Refill log —
   One line per link tried, arriving as each site answers. Which way it went
   has to be readable at a glance down a column of five. */
.rb-refill-log, .rb-refill-list { list-style: none; padding: 0; margin: 0; }
.rb-refill-log li, .rb-refill-list li {
  padding: var(--space-3) 0 var(--space-3) var(--space-5);
  border-bottom: 1px dotted var(--rule);
  font-size: 14px;
}
.rb-refill-log li.is-ok { box-shadow: inset 3px 0 0 #6a9a6a; }
.rb-refill-log li.is-no { box-shadow: inset 3px 0 0 #c0705f; }
.rb-refill-log li:last-child, .rb-refill-list li:last-child { border-bottom: 0; }

/* On a phone the course and date columns are the first things worth losing:
   the title and what is wrong with it are the whole point. */
@media (max-width: 720px) {
  .rb-review-table th:nth-child(4), .rb-review-table td:nth-child(4),
  .rb-review-table th:nth-child(5), .rb-review-table td:nth-child(5) { display: none; }
}

/* Deploy stamp, matching the main site's. Kept legible on a phone rather than
   the near-invisible 11px/.6 it used to be inline — this is the line you check
   to see whether a change actually went live. */
.rb-version { font-size: 12px; color: var(--color-text-quiet); }
