/*
 * Application shell and shared state vocabulary.
 *
 * Consumes tokens.css only. Every value here is a token or a structural
 * dimension; a literal colour or type size in this file is drift.
 *
 * This is the R1 layer. The legacy app.css still styles page interiors that
 * later milestones replace; nothing here reaches into those.
 */

/* ---- Page frame -------------------------------------------------------- */
body {
  background: var(--c-ground);
  color: var(--c-ink);
  font-family: var(--f-sans);
  font-size: var(--t-body);
}

/* A page whose <h1> says exactly what the mobile top bar already says.
   The bar carries the page title at 390 by design (DESIGN.md, shell at 390),
   so the heading beneath it is the redundant one — "Library" was rendered
   twice, and so were "Add a recipe" and "Approvals". It is hidden visually
   and kept in the accessibility tree, because dropping the <h1> would cost
   the page its heading; C2 chose the rail rename for the same reason and that
   fix only ever worked at widths where the rail exists.

   Marked per page rather than compared at runtime: which surfaces echo their
   title is a design decision about each screen, and the recipe page must NOT
   be marked — its bar says "Recipe" and its heading names the dish. */
@media (max-width: 700px) {
  .v2-title-echo {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

.v2-vh {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link: the first focusable element on every page. */
.v2-skip {
  position: absolute;
  left: var(--s-sm);
  top: -4rem;
  z-index: 90;
  background: var(--c-green);
  color: var(--c-surface);
  padding: var(--s-sm) var(--s-md);
  border-radius: var(--r-sm);
  text-decoration: none;
  min-height: var(--target);
  display: inline-flex;
  align-items: center;
}
.v2-skip:focus { top: var(--s-sm); }

:focus-visible {
  outline: 2px solid var(--c-green);
  outline-offset: 2px;
}

.v2-i {
  width: 20px; height: 20px;
  flex: 0 0 20px;
  stroke: currentColor;
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ---- Shell layout ------------------------------------------------------ */
#v2-app {
  display: flex;
  min-height: 100vh;
  align-items: stretch;
}
#v2-header { display: contents; }

.v2-rail {
  width: 236px;
  flex: 0 0 236px;
  background: var(--c-sink);
  border-right: 1px solid var(--c-hairline);
  padding: var(--s-md) var(--s-sm);
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.v2-rail-brandrow {
  display: flex;
  align-items: center;
  gap: var(--s-xs);
  margin-bottom: var(--s-sm);
}

/* The wordmark is the mark, not text. Clear space equals the width of its `I`
 * (STILL_BRAND.md); at this size that is close enough to --s-sm. */
.v2-brand {
  display: flex;
  align-items: center;
  text-decoration: none;
  padding: var(--s-sm);
  min-height: var(--target);
  min-width: var(--target);
  flex: 1;
}
.v2-wordmark {
  /* Minimum displayed width for the plain wordmark is 96px. The expanded rail
   * is 236px wide, so 128px sits comfortably above the floor; below 96px the
   * brand doc requires the framed S instead of a compressed wordmark. */
  width: 128px;
  height: auto;
  display: block;
}
.v2-appmark {
  display: none;
  width: 40px;
  height: 40px;
}
/* No shadow, radius, outline or badge may be drawn onto the artwork. Focus
 * indication belongs to the link. */
.v2-brand:focus-visible {
  outline: 2px solid var(--c-green);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

.v2-rail-toggle {
  position: relative;
  flex: 0 0 var(--target);
  width: var(--target);
  height: var(--target);
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: transparent;
  color: var(--c-muted);
  display: grid;
  place-items: center;
  cursor: pointer;
}
.v2-rail-toggle:hover {
  background: var(--c-sink-hover);
  color: var(--c-ink);
}

.v2-nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: var(--s-sm);
  border-radius: var(--r-md);
  text-decoration: none;
  color: var(--c-ink-2);
  font-size: var(--t-small);
  min-height: var(--target);
}
.v2-nav-item:hover { background: var(--c-sink-hover); }
.v2-nav-item[aria-current] {
  background: var(--c-green-tint);
  color: var(--c-green);
  font-weight: 600;
}

.v2-rail-group {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-brown);
  padding: var(--s-md) var(--s-sm) var(--s-xs);
}
.v2-rail-rule {
  border: 0;
  border-top: 1px solid var(--c-hairline);
  margin: var(--s-md) var(--s-sm);
}

.v2-rail-foot {
  margin-top: auto;
  padding-top: var(--s-sm);
  border-top: 1px solid var(--c-hairline);
  display: flex;
  flex-direction: column;
  gap: var(--s-sm);
  align-items: flex-start;
}
.v2-who { font-size: var(--t-meta); color: var(--c-muted); padding: 0 var(--s-sm); }

.v2-langswitch { display: flex; gap: var(--s-xs); padding: 0 var(--s-sm); }
.v2-lang {
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  color: var(--c-ink-2);
  font: inherit;
  font-size: var(--t-micro);
  min-height: var(--target);
  min-width: var(--target);
  padding: 0 var(--s-sm);
  cursor: pointer;
}
.v2-lang[aria-pressed='true'] {
  background: var(--c-green);
  border-color: var(--c-green);
  color: var(--c-surface);
  font-weight: 600;
}
.v2-signout { margin: 0 var(--s-sm); }

/*
 * Desktop collapse is deliberately immediate: animating width forces the
 * reading column and its images to re-layout on every frame. The compact rail
 * uses the approved framed S because the plain wordmark cannot be compressed
 * below its 96px minimum.
 */
.v2-rail[data-collapsed='true'] {
  width: 64px;
  flex-basis: 64px;
}
.v2-rail[data-collapsed='true'] .v2-rail-brandrow {
  flex-direction: column;
}
.v2-rail[data-collapsed='true'] .v2-brand {
  flex: 0 0 var(--target);
  width: var(--target);
  padding: 2px;
}
.v2-rail[data-collapsed='true'] .v2-wordmark { display: none; }
.v2-rail[data-collapsed='true'] .v2-appmark { display: block; }
.v2-rail[data-collapsed='true'] .v2-nav-item {
  justify-content: center;
  padding-left: 0;
  padding-right: 0;
}
.v2-rail[data-collapsed='true'] .v2-rail-group,
.v2-rail[data-collapsed='true'] .v2-who {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.v2-rail[data-collapsed='true'] .v2-rail-rule {
  margin-left: var(--s-xs);
  margin-right: var(--s-xs);
}
.v2-rail[data-collapsed='true'] .v2-rail-foot {
  align-items: center;
  width: 100%;
}
.v2-rail[data-collapsed='true'] .v2-langswitch {
  flex-direction: column;
  padding: 0;
}
.v2-rail[data-collapsed='true'] .v2-signout {
  position: relative;
  width: var(--target);
  padding: 0;
  margin: 0;
}

/*
 * The actual translated label is the tooltip. It stays in the DOM, while an
 * explicit aria-label keeps the control named even while this visible copy is
 * clipped. Both pointer and keyboard users reveal the same wording.
 */
.v2-rail-toggle .v2-control-label,
.v2-rail[data-collapsed='true'] .v2-nav-label,
.v2-rail[data-collapsed='true'] .v2-signout .v2-control-label {
  position: absolute;
  left: calc(100% + var(--s-sm));
  top: 50%;
  z-index: 30;
  width: max-content;
  max-width: 18rem;
  padding: var(--s-xs) var(--s-sm);
  border-radius: var(--r-sm);
  background: var(--c-ink);
  color: var(--c-surface);
  font-size: var(--t-micro);
  font-weight: 400;
  line-height: 1.4;
  white-space: nowrap;
  transform: translateY(-50%);
  visibility: hidden;
}
.v2-rail[data-collapsed='true'] .v2-nav-item:hover .v2-nav-label,
.v2-rail[data-collapsed='true'] .v2-nav-item:focus .v2-nav-label,
.v2-rail-toggle:hover .v2-control-label,
.v2-rail-toggle:focus .v2-control-label,
.v2-rail[data-collapsed='true'] .v2-signout:hover .v2-control-label,
.v2-rail[data-collapsed='true'] .v2-signout:focus .v2-control-label {
  visibility: visible;
}

/* Content column */
#v2-main, .v2-main {
  flex: 1;
  min-width: 0;
  width: 100%;
  padding: var(--s-lg) var(--s-xl) var(--s-xl);
  /* The shell owns the available workspace. Reading surfaces constrain their
     own prose (`.v2-reading`); capping the shared flex item left large dead
     areas beside a collapsed rail and forced review/editor workspaces to
     scroll horizontally before they needed to. */
  max-width: none;
}

/* ---- Mobile chrome (hidden at desktop) --------------------------------- */
.v2-mobtop, .v2-bottomnav { display: none; }

/* ---- Icon button ------------------------------------------------------- */
.v2-iconbtn {
  background: none;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  color: var(--c-ink-2);
  min-width: var(--target);
  min-height: var(--target);
  display: none;              /* mobile chrome; revealed in the narrow block */
  place-items: center;
  cursor: pointer;
  font: inherit;
}
.v2-iconbtn:hover { background: var(--c-sink-hover); }

/* ---- Sheet (mobile dialog) --------------------------------------------- */
.v2-sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 60;
  background: var(--c-surface);
  border-top: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -12px 40px rgb(23 21 15 / 22%);
  padding: var(--s-sm) var(--s-md) calc(var(--s-md) + env(safe-area-inset-bottom));
  max-height: 72vh;
  overflow: auto;
}
.v2-sheet[hidden] { display: none; }
.v2-sheet-head {
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  padding-bottom: var(--s-sm);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-sheet-head h2 {
  font-family: var(--f-serif);
  font-size: var(--t-lead);
  font-weight: 400;
  margin: 0;
  flex: 1;
}
.v2-sheet-head .v2-iconbtn { display: grid; }
.v2-sheet-item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 52px;
  padding: var(--s-sm) var(--s-xs);
  border: 0;
  border-bottom: 1px solid var(--c-hairline);
  background: none;
  color: var(--c-ink);
  font: inherit;
  font-size: var(--t-body);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.v2-sheet-item:hover { background: var(--c-sink-hover); }
.v2-sheet-signout { color: var(--c-alarm); }
.v2-sheet-row {
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  flex-wrap: wrap;
  padding: var(--s-sm) var(--s-xs);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-sheet-rowlabel { font-size: var(--t-micro); color: var(--c-muted); flex: 1; }

/* ---- Buttons ----------------------------------------------------------- */
.v2-btn {
  font: inherit;
  font-size: var(--t-small);
  border-radius: var(--r-sm);
  border: 1px solid var(--c-hairline-strong);
  background: var(--c-surface);
  color: var(--c-ink-2);
  min-height: var(--target);
  padding: 0 var(--s-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-sm);
  cursor: pointer;
  text-decoration: none;
}
.v2-btn-primary {
  background: var(--c-green);
  border-color: var(--c-green);
  color: var(--c-surface);
}
.v2-btn-block { width: 100%; }
/* Disabled must LOOK disabled. Opacity alone left a saturated primary button
 * indistinguishable from an active one. */
.v2-btn[disabled], .v2-btn[aria-disabled='true'] {
  background: var(--c-sink);
  border-color: var(--c-hairline-strong);
  color: var(--c-muted);
  cursor: not-allowed;
}
.v2-actions { display: flex; gap: var(--s-sm); flex-wrap: wrap; margin-top: var(--s-md); }

/* ---- States ------------------------------------------------------------ */
.v2-sk {
  background: var(--c-skeleton);
  border-radius: var(--r-xs);
  height: 0.85rem;
  margin-bottom: var(--s-sm);
}
.v2-sk-img { height: auto; aspect-ratio: 3 / 2; margin-bottom: var(--s-md); }

.v2-empty {
  border: 1px solid var(--c-hairline);
  background: var(--c-surface);
  padding: var(--s-lg);
}
.v2-empty-title { font-family: var(--f-serif); font-size: var(--t-section); font-weight: 400; margin: 0; }
.v2-empty-body { color: var(--c-muted); font-size: var(--t-meta); margin: var(--s-sm) 0 0; }

/* No coloured SIDE border carries meaning anywhere: state rides on the top
 * rule, the wording and the notation mark. */
.v2-notice {
  border: 1px solid var(--c-hairline-strong);
  border-top-width: 3px;
  background: var(--c-surface);
  padding: var(--s-md);
  margin-bottom: var(--s-md);
}
.v2-notice-title { font-size: var(--t-small); font-weight: 600; margin: 0; }
.v2-notice-body { font-size: var(--t-meta); margin: var(--s-xs) 0 0; }
.v2-notice-error { border-top-color: var(--c-alarm); background: var(--c-alarm-tint); }
.v2-notice-error .v2-notice-title { color: var(--c-alarm); }
.v2-notice-success { border-top-color: var(--c-green); background: var(--c-green-tint); }
.v2-notice-info { border-top-color: var(--c-info); background: var(--c-info-tint); }
.v2-notice-caution { border-top-color: var(--c-yellow); background: var(--c-yellow-tint); }
.v2-notice-caution .v2-notice-title { color: var(--c-yellow-ink); }

/* Which language this recipe is being read in. Blue, because this system gives
 * blue to informational and translation state and to nothing else — a reviewed
 * translation is a normal way to read a recipe, never a caution. */
#v2-translation { margin-top: var(--s-md); }
#v2-translation .v2-notice-body { max-width: 62ch; }
#v2-translation .v2-actions { margin-top: var(--s-sm); }

/* A surface introducing itself on first use (UX-11). Deliberately quieter than
   a notice that reports something: this is page furniture a person reads once,
   not an event. The body wraps at a readable measure rather than the full
   content width, because it is prose and the rest of the page is not. */
/* Reference photographs for one instruction step. They sit under their step
   text and are deliberately modest: this is a "does mine look like that?"
   glance mid-task, not a gallery. max-width keeps a large upload from
   dominating the method, and the aspect ratio is the image's own — a cooking
   stage cropped to a uniform box can stop showing the thing it was taken for. */
.v2-stepshots {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-md);
  margin: var(--s-sm) 0 var(--s-md);
}
.v2-stepshot { margin: 0; flex: 0 1 auto; max-width: min(100%, 22rem); }
.v2-stepshot-img {
  display: block;
  /* max-width, not width: a reference photo is never upscaled. Someone
     photographing a pan mid-step may upload something small, and stretching it
     to fill the column makes the one thing it was taken to show — what the food
     looks like — blurrier than the original. It shrinks to fit a narrow column
     and otherwise stays its own size. */
  max-width: 100%;
  height: auto;
  border: 1px solid var(--c-hairline);
  background: var(--c-skeleton);
}
.v2-stepshot-caption {
  font-size: var(--t-meta);
  color: var(--c-muted);
  margin-top: var(--s-xs);
}

/* Attaching a photograph to one instruction step, in the editor. */
.v2-stepimage { margin-top: var(--s-sm); }
.v2-stepimage-row {
  display: flex;
  gap: var(--s-md);
  align-items: flex-start;
  padding: var(--s-sm) 0;
  border-top: 1px solid var(--c-hairline);
}
.v2-stepimage-thumb {
  flex: 0 0 auto;
  width: 96px;
  height: 72px;
  object-fit: cover;
  border: 1px solid var(--c-hairline);
  background: var(--c-skeleton);
}
/* A photo that is uploaded but not yet saved has no servable row_id, so it
   shows a placeholder rather than a guessed URL that would 404. */
.v2-stepimage-pending { display: block; }
.v2-stepimage-fields { flex: 1 1 auto; min-width: 0; }
.v2-stepimage-note { margin: var(--s-xs) 0 0; font-size: var(--t-meta); }

.v2-intro { margin-bottom: var(--s-lg); }
.v2-intro .v2-notice-body { max-width: 62ch; }
.v2-intro .v2-actions { margin-top: var(--s-sm); }

.v2-disabled-action { display: flex; flex-direction: column; gap: var(--s-xs); align-items: flex-start; }
.v2-reason {
  font-size: var(--t-micro);
  color: var(--c-muted);
  margin: 0;
  max-width: 40ch;
}

/* ---- Login gate -------------------------------------------------------- */
#v2-gate { padding: var(--s-xl) var(--s-md); display: flex; justify-content: center; }
.v2-gate-card {
  width: 100%;
  max-width: 24rem;
  background: var(--c-surface);
  border: 1px solid var(--c-hairline);
  padding: var(--s-lg);
}
.v2-gate-title { font-family: var(--f-serif); font-size: var(--t-page); font-weight: 400; margin: 0 0 var(--s-md); }
.v2-gate-intro { margin: calc(-1 * var(--s-xs)) 0 var(--s-md); color: var(--c-ink-2); }
.v2-gate-language { margin: 0 0 var(--s-md); }
.v2-gate-language .v2-langswitch { padding: 0; }

.v2-login-gate {
  position: relative;
}
.v2-login-gate > .v2-gate-language {
  position: absolute;
  top: var(--s-md);
  right: var(--s-md);
  z-index: 4;
  margin: 0;
}
.v2-login-gate .v2-gate-title {
  padding-right: calc(var(--target) + var(--s-sm));
}
.v2-login-lang {
  position: relative;
  display: block;
}
.v2-login-lang summary {
  list-style: none;
}
.v2-login-lang summary::-webkit-details-marker {
  display: none;
}
.v2-login-lang-trigger {
  min-width: var(--target);
  min-height: var(--target);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-xs);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  color: var(--c-ink-2);
  background: var(--c-surface);
  cursor: pointer;
  font: inherit;
  font-size: var(--t-micro);
}
.v2-login-lang-trigger:hover,
.v2-login-lang[open] .v2-login-lang-trigger {
  background: var(--c-sink);
}
.v2-login-lang-trigger:focus-visible,
.v2-login-lang-option:focus-visible {
  outline: 2px solid var(--c-green);
  outline-offset: 2px;
}
.v2-login-lang-trigger .v2-i {
  width: 20px;
  height: 20px;
}
.v2-login-lang-menu {
  position: absolute;
  top: calc(100% + var(--s-xs));
  right: 0;
  min-width: 11rem;
  padding: var(--s-xs);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-md);
  box-shadow: 0 12px 28px rgb(23 21 15 / 14%);
}
.v2-login-lang-option {
  position: relative;
  width: 100%;
  min-height: var(--target);
  display: flex;
  align-items: center;
  padding: 0 calc(var(--s-lg) + var(--s-xs)) 0 var(--s-sm);
  border: 0;
  border-radius: var(--r-sm);
  color: var(--c-ink-2);
  background: transparent;
  cursor: pointer;
  font: inherit;
  font-size: var(--t-small);
  text-align: left;
}
.v2-login-lang-option:hover {
  background: var(--c-sink);
}
.v2-login-lang-option[aria-checked='true'] {
  color: var(--c-green);
  background: var(--c-green-tint);
  font-weight: 600;
}
.v2-login-lang-option[aria-checked='true']::after {
  content: '';
  position: absolute;
  right: var(--s-md);
  width: 6px;
  height: 11px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
}

.v2-gate-help,
.v2-invite-recovery { margin-top: var(--s-lg); padding-top: var(--s-md); border-top: 1px solid var(--c-hairline); }
.v2-gate-help-title { margin: 0 0 var(--s-xs); font-family: var(--f-serif); font-size: var(--t-section); font-weight: 400; }
.v2-gate-help p,
.v2-invite-recovery .v2-muted { margin: 0; overflow-wrap: anywhere; }
.v2-invite-recovery .v2-btn { margin-top: var(--s-md); }
.v2-field { margin-bottom: var(--s-md); }
.v2-field label {
  display: block;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-brown);
  margin-bottom: var(--s-xs);
}
.v2-field input {
  width: 100%;
  font: inherit;
  min-height: var(--target);
  padding: 0 var(--s-sm);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  background: var(--c-surface);
  color: var(--c-ink);
}

/* ---- 390 px: structural rearrangement, not a shrink -------------------- */
@media (max-width: 700px) {
  #v2-app {
    /* 1px rule + 56px labelled target + 8px vertical padding + safe area.
     * Reserve the fixed navigation's full block so the end of the document is
     * never hidden behind it. */
    --v2-bottomnav-block: calc(1px + 56px + var(--s-sm) + env(safe-area-inset-bottom));
    flex-direction: column;
    min-height: 100vh;
    padding-bottom: var(--v2-bottomnav-block);
  }

  /* The rail is replaced, not squeezed. */
  .v2-rail { display: none; }

  .v2-mobtop {
    display: flex;
    align-items: center;
    gap: var(--s-sm);
    padding: var(--s-sm) var(--s-md);
    background: var(--c-surface);
    border-bottom: 1px solid var(--c-hairline);
    position: sticky;
    top: 0;
    z-index: 20;
  }
  .v2-mobtitle {
    font-family: var(--f-serif);
    font-size: var(--t-lead);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Account reach on a phone: language, personal areas and sign-out live
   * behind this, because the rail that normally holds them is gone. */
  .v2-mobtop .v2-iconbtn { display: grid; }
  .v2-mobback-spacer {
    display: block;
    width: var(--target);
    height: var(--target);
    flex: 0 0 var(--target);
  }

  .v2-bottomnav {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 20;
    min-height: var(--v2-bottomnav-block);
    background: var(--c-surface);
    border-top: 1px solid var(--c-hairline-strong);
    padding: var(--s-xs) var(--s-xs) calc(var(--s-xs) + env(safe-area-inset-bottom));
  }
  /* Labelled, never icon-only: an unlabelled icon needs product knowledge. */
  .v2-bottomnav .v2-nav-item {
    flex: 1;
    flex-direction: column;
    gap: 2px;
    font-size: var(--t-eyebrow);
    letter-spacing: normal;
    text-transform: none;
    min-height: 56px;
    justify-content: center;
    text-align: center;
    padding: var(--s-xs) 2px;
  }
  .v2-bottomnav .v2-nav-label { overflow: hidden; text-overflow: ellipsis; max-width: 100%; }

  #v2-main, .v2-main { padding: var(--s-md) var(--s-md) var(--s-xl); max-width: none; }
  #v2-gate { padding: var(--s-md); }
  .v2-gate-card { border-left: 0; border-right: 0; }
}

/* ======================================================================
   Library
   Token-only. Every value below resolves to a DESIGN.md token; a literal
   here would be drift, not a decision.
   ====================================================================== */
.v2-page-title {
  font-family: var(--f-serif);
  font-size: var(--t-page);
  font-weight: 400;
  line-height: 1.15;
  margin: 0 0 var(--s-md);
}
.v2-library-primary {
  display: flex;
  gap: var(--s-md);
  align-items: flex-end;
  flex-wrap: wrap;
}
.v2-toolbar {
  display: flex;
  gap: var(--s-sm) var(--s-md);
  align-items: flex-end;
  flex-wrap: wrap;
}
.v2-searchfield { flex: 1 1 22rem; min-width: 0; }
.v2-library-primary .v2-new { flex: 0 0 auto; }
.v2-label {
  display: block;
  font-size: var(--t-eyebrow);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-brown);
  margin-bottom: var(--s-xs);
}
.v2-input {
  width: 100%;
  font: inherit;
  font-size: var(--t-body);
  color: var(--c-ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--c-ink);
  border-radius: 0;
  padding: var(--s-sm) var(--s-xs);
  min-height: var(--target);
}
.v2-input:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-check { display: flex; align-items: center; min-height: var(--target); }
/* A 20px box with a 44px TARGET. `appearance: none` makes the input a normal
   element, so its own box can be the full target while ::before draws the
   control at conventional size. A default-appearance input is replaced content
   and cannot carry ::before at all. */
.v2-check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  /* A flex item SHRINKS by default, so `width` alone is a starting size, not a
     floor: in a narrow column these measured 41px. The target has to be stated
     as a non-shrinking basis. */
  flex: 0 0 var(--target);
  width: var(--target);
  min-width: var(--target);
  height: var(--target);
  margin: 0;
  display: grid;
  place-content: center;
  cursor: pointer;
  background: none;
  border: 0;
}
.v2-check input[type="checkbox"]::before {
  content: '';
  width: 20px;
  height: 20px;
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-xs);
  background: var(--c-surface);
}
.v2-check input[type="checkbox"]:checked::before {
  background: var(--c-green);
  border-color: var(--c-green);
  box-shadow: inset 0 0 0 3px var(--c-surface);
}
.v2-check input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--c-green);
  outline-offset: -6px;
  border-radius: var(--r-sm);
}
.v2-check label {
  font-size: var(--t-small);
  color: var(--c-ink-2);
  display: flex;
  align-items: center;
  min-height: var(--target);
  cursor: pointer;
}
.v2-note {
  font-size: var(--t-micro);
  color: var(--c-muted);
  margin: var(--s-xs) 0 0;
}
.v2-new { flex: 0 0 auto; text-decoration: none; }

/* The toolbar is one row: the filter disclosure, sort, and the view toggle.
   It replaced a five-column grid of labelled selects that occupied the whole
   first screen on a phone before a single recipe appeared. */
.v2-toolbar {
  display: flex;
  gap: var(--s-sm) var(--s-md);
  align-items: flex-end;
  flex-wrap: wrap;
  margin-top: var(--s-lg);
  padding: var(--s-md) 0;
  border-top: 1px solid var(--c-hairline);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-sortfield { min-width: 0; flex: 0 1 14rem; }

/* Sort and density share one wrapper so they can be relocated as a pair at 390
   without being duplicated. At desktop the wrapper is not a box at all — its
   children are direct flex items of the toolbar, exactly as before. */
.v2-viewsort { display: contents; }
.v2-viewsortbtn,
.v2-viewsort-title,
.v2-viewsort-close { display: none; }
.v2-filterbtn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-sm);
  min-height: var(--target);
  margin-right: auto;
}
.v2-filterbtn[aria-expanded="true"] { background: var(--c-green-tint); color: var(--c-green); }
/* The count is the whole point of a collapsed disclosure: it says how much is
   hidden behind it without opening it. */
.v2-filtercount {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding: 0 var(--s-xs);
  border-radius: 999px;
  background: var(--c-green);
  color: var(--c-surface);
  font-size: var(--t-micro);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.v2-filtercount[hidden] { display: none; }

.v2-filterpanel {
  display: grid;
  gap: var(--s-md);
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-filterpanel[hidden] { display: none; }
.v2-filtergroup { min-width: 0; }
.v2-chiprow { display: flex; flex-wrap: wrap; gap: var(--s-sm); }
/* A chip is a 44px target with a visible pressed state — no hover-only
   function, no 4px hit area (DESIGN.md rules 4 and 6). */
.v2-fchip,
.v2-chipmore {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  padding: var(--s-xs) var(--s-md);
  border: 1px solid var(--c-hairline-strong);
  border-radius: 999px;
  background: var(--c-surface);
  color: var(--c-ink-2);
  font: inherit;
  font-size: var(--t-small);
  cursor: pointer;
}
.v2-fchip:hover, .v2-chipmore:hover { border-color: var(--c-brown); color: var(--c-ink); }
.v2-fchip:focus-visible,
.v2-chipmore:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-fchip[aria-pressed="true"] {
  background: var(--c-green);
  border-color: var(--c-green);
  color: var(--c-surface);
  font-weight: 600;
}
.v2-chipmore { border-style: dashed; }

.v2-select {
  display: block;
  width: 100%;
  min-height: var(--target);
  color: var(--c-ink);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  padding: var(--s-sm);
  font: inherit;
  font-size: var(--t-small);
}
.v2-select:hover { border-color: var(--c-brown); }
.v2-select:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-favourites-filter { align-self: end; }
.v2-density {
  display: flex;
  align-items: end;
  gap: 0;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.v2-density legend { width: 100%; }
.v2-density label { position: relative; margin: 0; }
.v2-density input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.v2-density span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--target);
  padding: var(--s-sm) var(--s-md);
  border: 1px solid var(--c-hairline-strong);
  background: var(--c-surface);
  color: var(--c-ink-2);
  font-size: var(--t-small);
}
.v2-density label:first-of-type span { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.v2-density label:last-of-type span { border-radius: 0 var(--r-sm) var(--r-sm) 0; border-left: 0; }
.v2-density input:checked + span { background: var(--c-green-tint); color: var(--c-green); font-weight: 600; }
.v2-density input:focus-visible + span { outline: 2px solid var(--c-green); outline-offset: 2px; z-index: 1; }

.v2-filtersummary {
  display: flex;
  gap: var(--s-sm);
  align-items: center;
  flex-wrap: wrap;
  margin-top: var(--s-md);
  padding: var(--s-sm) var(--s-md);
  background: var(--c-sink);
  border: 1px solid var(--c-hairline);
}
.v2-filtersummary[hidden] { display: none; }
.v2-clearall { margin-left: auto; }

.v2-countline {
  display: flex;
  align-items: baseline;
  gap: var(--s-sm);
  margin: var(--s-md) 0 var(--s-lg);
  padding-bottom: var(--s-sm);
  border-bottom: 1px solid var(--c-hairline-strong);
}
.v2-count { font-weight: 600; font-variant-numeric: tabular-nums; }

/* Adaptive: three columns where the width allows, without guessing breakpoints. */
.v2-results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: var(--s-xl) var(--s-lg);
}
.v2-results-list {
  display: block;
}
.v2-results-list .v2-entry {
  display: grid;
  grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
  gap: var(--s-sm) var(--s-lg);
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-results-list .v2-rlink {
  display: grid;
  grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  gap: 0 var(--s-lg);
  grid-column: 1 / -1;
  padding-right: calc(var(--target) + var(--s-md));
}
.v2-results-list .v2-shot,
.v2-results-list .v2-plate { grid-row: 1 / 3; }
.v2-results-list .v2-notation { grid-column: 2; align-self: end; }
.v2-results-list .v2-entry-title { grid-column: 2; align-self: start; }
.v2-results-list .v2-entry-foot,
.v2-results-list .v2-taglist { grid-column: 2; }
.v2-results-list .v2-fav { top: var(--s-md); right: 0; }
.v2-entry { position: relative; display: flex; flex-direction: column; gap: var(--s-sm); min-width: 0; }

/* Image AND title in one link. The favourite control lives outside it. */
.v2-rlink { display: block; text-decoration: none; color: inherit; }
.v2-rlink:hover .v2-entry-title { text-decoration: underline; text-underline-offset: 3px; }
.v2-rlink:focus-visible { outline: 2px solid var(--c-green); outline-offset: 3px; }
.v2-shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--c-sink);
}
/* No photograph is a designed state, not a gap: the title set in the serif
   inside a hairline frame, holding the grid's rhythm. */
.v2-plate {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  padding: var(--s-md);
  text-align: center;
}
.v2-plate-title {
  font-family: var(--f-serif);
  font-size: var(--t-section);
  line-height: 1.2;
  color: var(--c-ink);
}
/* Names the state, so the title above it reads as a stand-in for the missing
   photograph rather than as the same heading printed twice. */
.v2-plate-note {
  display: block;
  margin-top: var(--s-xs);
  font-size: var(--t-micro);
  color: var(--c-muted);
}
.v2-notation {
  display: inline-block;
  margin-top: var(--s-sm);
  font-size: var(--t-eyebrow);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
}
/* Review states. Green is the only accent that means "done"; everything in
   flight is brown, and only a genuine stop is alarm. Blue is informational and
   carries no action. None of these is load-bearing on its own: the state is
   always spelled out as a word next to it. */
.v2-state-approved { color: var(--c-green); }
.v2-state-draft { color: var(--c-brown); }
.v2-state-ready { color: var(--c-green); }
.v2-state-submitted { color: var(--c-info); }
.v2-state-in_review { color: var(--c-info); }
.v2-state-changes_requested { color: var(--c-brown); }
.v2-state-rejected { color: var(--c-alarm); }
.v2-state-withdrawn { color: var(--c-muted); }
.v2-entry-title {
  font-family: var(--f-serif);
  font-size: var(--t-section);
  font-weight: 400;
  line-height: 1.25;
  margin: var(--s-xs) 0 0;
}

.v2-entry-foot { display: flex; align-items: flex-start; gap: var(--s-sm); }
/* Named rows, never a dot-separated chain: two same-titled recipes must be
   distinguishable without opening either. */
.v2-idlines {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px var(--s-sm);
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: var(--t-meta);
}
.v2-idlines dt {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-brown);
  padding-top: 2px;
}
.v2-idlines dd { margin: 0; color: var(--c-ink-2); min-width: 0; }

/* Overlays the image's top-right corner, so it costs no vertical space.
   It needs its own opaque ground: photographs run from near-white (a bowl of
   foam) to near-black (a dark plate), and a bare glyph would be illegible on
   one or the other. The surface token gives it a constant contrast pairing
   instead of hoping the photo cooperates. */
.v2-fav {
  position: absolute;
  top: var(--s-sm);
  right: var(--s-sm);
  z-index: 2;
  min-width: var(--target);
  min-height: var(--target);
  display: grid;
  place-items: center;
  font-size: var(--t-lead);
  line-height: 1;
  color: var(--c-muted);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.v2-fav[aria-pressed="true"] { color: var(--c-brown); }
.v2-fav:hover { border-color: var(--c-brown); }
.v2-fav:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }

.v2-taglist { display: flex; gap: var(--s-xs); flex-wrap: wrap; margin: 0; }
.v2-tag {
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-pill);
  padding: var(--s-xs) var(--s-sm);
  font-size: var(--t-micro);
  color: var(--c-ink-2);
  min-height: var(--target);
  cursor: pointer;
}
.v2-tag:hover { border-color: var(--c-green); color: var(--c-green); }

.v2-pagination {
  display: flex;
  justify-content: center;
  margin: var(--s-xl) 0 0;
}
.v2-pagination:empty { display: none; }
.v2-pagination .v2-notice { width: 100%; }

@media (max-width: 700px) {
  .v2-results { grid-template-columns: 1fr; gap: var(--s-xl); }
  .v2-toolbar { gap: var(--s-sm); }
  .v2-library-primary { align-items: stretch; flex-wrap: wrap; }
  .v2-searchfield { flex: 1 1 100%; }
  .v2-new { flex: 1 1 100%; justify-content: center; }
  /* On a phone the toolbar must stay ONE line: the disclosure keeps its place
     beside sort and the view toggle, and none of them earns a row of its own
     above the first recipe. */
  .v2-toolbar { padding: var(--s-sm) 0; align-items: center; flex-wrap: nowrap; }
  /* Measured: the bar is 358px, and Filters + sort + Gallery + List need 366px
     with the select already clipping "Newest recipe" (104.6px of text in a
     66.2px box). Showing its longest option would need 454px. Four controls do
     not fit at any label length, so two of them move behind one. */
  .v2-viewsortbtn { display: inline-flex; flex: 0 0 auto; }
  .v2-viewsort {
    display: block;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 60;
    max-height: 72vh;
    overflow-y: auto;
    background: var(--c-surface);
    border-top: 1px solid var(--c-hairline-strong);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    box-shadow: 0 -12px 40px rgb(23 21 15 / 22%);
    padding: var(--s-md) var(--s-md) calc(var(--s-md) + env(safe-area-inset-bottom));
  }
  .v2-viewsort-title {
    display: block;
    margin: 0 0 var(--s-md);
    font-family: var(--f-serif);
    font-size: var(--t-section);
    font-weight: 400;
  }
  /* In the toolbar these names had nowhere to go and were visually hidden. The
     sheet has room, so the controls get their labels back. */
  .v2-viewsort .v2-sortfield .v2-label,
  .v2-viewsort .v2-density legend {
    position: static;
    width: auto; height: auto;
    margin: 0 0 var(--s-xs);
    padding: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    display: block;
    font-size: var(--t-eyebrow);
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--c-brown);
    font-weight: 600;
  }
  .v2-viewsort .v2-sortfield { flex: none; margin-bottom: var(--s-lg); }
  .v2-viewsort .v2-select { width: 100%; }
  .v2-viewsort .v2-density { display: block; }
  .v2-viewsort .v2-density > label { display: block; }
  .v2-viewsort .v2-density span { width: 100%; justify-content: flex-start; }
  .v2-viewsort .v2-density label:first-of-type span { border-radius: var(--r-sm) var(--r-sm) 0 0; }
  .v2-viewsort .v2-density label:last-of-type span {
    border-radius: 0 0 var(--r-sm) var(--r-sm);
    border-left: 1px solid var(--c-hairline-strong);
    border-top: 0;
  }
  .v2-viewsort-close { display: flex; width: 100%; margin-top: var(--s-lg); justify-content: center; }
  .v2-filterbtn { flex: 0 0 auto; margin-right: 0; }
  .v2-sortfield { flex: 1 1 auto; }
  /* Hidden from sight, not from the accessibility tree: `display: none` on a
     <label> or a <legend> takes the control's and the group's name with it. */
  .v2-sortfield .v2-label,
  .v2-density legend {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .v2-density { align-items: center; flex: 0 0 auto; }
  .v2-density label { flex: 1; }
  .v2-density span { width: 100%; }
  .v2-filtersummary { flex-wrap: nowrap; overflow-x: auto; }
  .v2-clearall { margin-left: 0; flex: 0 0 auto; }
  .v2-results-list { display: block; }
  .v2-results-list .v2-entry {
    grid-template-columns: 7rem minmax(0, 1fr);
    gap: var(--s-sm) var(--s-md);
  }
  .v2-results-list .v2-rlink {
    grid-template-columns: 7rem minmax(0, 1fr);
    gap: 0 var(--s-md);
  }
  .v2-results-list .v2-entry-foot,
  .v2-results-list .v2-taglist { grid-column: 1 / -1; }
}

/* ======================================================================
   Recipe view
   Reading column plus a permanently reserved comment gutter. The R3
   discussion column occupies the third track only while it is open, so the
   marker rail never shifts and the reading measure remains stable.
   ====================================================================== */
.v2-reading {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--s-gutter);
  column-gap: 0;
  align-items: start;
  max-width: 62rem;
}
.v2-reading > * { grid-column: 1; }
.v2-reading[data-discussion-open='true'] {
  grid-template-columns: minmax(55ch, 1fr) var(--s-gutter) minmax(18rem, 22rem);
  max-width: 86rem;
}
.v2-reading > .v2-discussion {
  grid-column: 3;
  grid-row: 1 / span 100;
}

.v2-backline { font-size: var(--t-meta); margin: 0 0 var(--s-md); }
.v2-backline a {
  color: var(--c-muted);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s-xs);
  min-height: var(--target);
  padding-right: var(--s-sm);
}
/* The arrow is an affordance, not copy: it stays out of the translated string
   and out of the accessible name, which the link text already carries. It also
   gives a short label like "Library" enough width to clear the target minimum. */
.v2-backline a::before { content: '\2190'; }
.v2-backline a:hover { color: var(--c-green); text-decoration: underline; }

.v2-hero {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--c-sink);
  margin-bottom: var(--s-md);
}
.v2-recipe-title {
  font-family: var(--f-serif);
  font-size: var(--t-recipe);
  font-weight: 400;
  line-height: 1.12;
  margin: var(--s-xs) 0 var(--s-sm);
}
.v2-recipe-lead {
  font-family: var(--f-serif);
  font-size: var(--t-lead);
  line-height: 1.5;
  margin: 0 0 var(--s-sm);
}
.v2-meta { font-size: var(--t-meta); color: var(--c-muted); margin: 0 0 var(--s-sm); }
.v2-readonly {
  font-size: var(--t-meta);
  color: var(--c-ink-2);
  background: var(--c-sink);
  border-top: 3px solid var(--c-brown);
  padding: var(--s-sm) var(--s-md);
  margin: 0 0 var(--s-md);
}
.v2-extlink { text-decoration: none; }

.v2-actions { display: flex; gap: var(--s-sm); flex-wrap: wrap; margin: var(--s-md) 0; }

.v2-scalebar {
  padding: 0 0 var(--s-md);
  margin-bottom: var(--s-md);
}
.v2-yield-control {
  position: relative;
  display: inline-block;
  max-width: 100%;
}
.v2-yield-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--s-sm);
  min-height: var(--target);
  max-width: 100%;
  padding: var(--s-xs) 0;
  border: 0;
  border-bottom: 1px solid var(--c-hairline-strong);
  background: transparent;
  color: var(--c-ink);
  font: 600 var(--t-body)/1.35 var(--f-sans);
  font-variant-numeric: tabular-nums;
  text-align: left;
  cursor: pointer;
}
.v2-yield-trigger:hover { border-bottom-color: var(--c-green); color: var(--c-green); }
.v2-yield-trigger:focus-visible,
.v2-scale-choice:focus-visible,
.v2-scale-exact-open:focus-visible {
  outline: 2px solid var(--c-green);
  outline-offset: 2px;
}
.v2-yield-trigger:disabled { color: var(--c-muted); cursor: wait; }
.v2-yield-chevron {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 160ms cubic-bezier(.16, 1, .3, 1);
}
.v2-yield-trigger[aria-expanded='true'] .v2-yield-chevron { transform: rotate(180deg); }
.v2-yield-trigger[aria-busy='true'] .v2-yield-chevron { display: none; }
.v2-yield-trigger[aria-busy='true']::after {
  content: '';
  width: 16px;
  height: 16px;
  border: 2px solid var(--c-hairline-strong);
  border-top-color: var(--c-green);
  border-radius: 50%;
  animation: v2-scale-spin 700ms linear infinite;
}
@keyframes v2-scale-spin { to { transform: rotate(360deg); } }
.v2-scale-popover {
  position: absolute;
  z-index: 55;
  top: calc(100% + var(--s-sm));
  left: 0;
  width: min(22rem, calc(100vw - 2 * var(--s-lg)));
  padding: var(--s-sm);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-md);
  background: var(--c-surface);
  box-shadow: 0 10px 30px rgb(23 21 15 / 10%);
  animation: v2-scale-reveal 160ms cubic-bezier(.16, 1, .3, 1) both;
}
@keyframes v2-scale-reveal {
  from { opacity: 0; clip-path: inset(0 0 18% 0); transform: translateY(-4px); }
}
.v2-scale-menu { display: grid; gap: 1px; }
.v2-scale-choice,
.v2-scale-exact-open {
  width: 100%;
  min-height: var(--target);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--c-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.v2-scale-choice {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr);
  align-items: center;
  gap: var(--s-md);
  padding: var(--s-sm) var(--s-md);
  font-variant-numeric: tabular-nums;
}
.v2-scale-choice:hover,
.v2-scale-exact-open:hover { background: var(--c-sink-hover); }
.v2-scale-choice[aria-checked='true'] { background: var(--c-green-tint); color: var(--c-green); }
.v2-scale-choice-factor { font-weight: 650; }
.v2-scale-preview { color: var(--c-muted); text-align: right; }
.v2-scale-choice[aria-checked='true'] .v2-scale-preview { color: var(--c-green); }
.v2-scale-exact-open {
  margin-top: var(--s-xs);
  padding: var(--s-sm) var(--s-md);
  border-top: 1px solid var(--c-hairline);
  border-radius: 0 0 var(--r-sm) var(--r-sm);
  color: var(--c-green);
  font-weight: 600;
}
.v2-scale-exact { padding: var(--s-sm); }
.v2-scale-exact-row { display: flex; align-items: flex-end; gap: var(--s-sm); }
.v2-scale-exact-row .v2-input {
  min-width: 0;
  font-variant-numeric: tabular-nums;
}
.v2-scale-unit {
  flex: 0 0 auto;
  min-height: var(--target);
  display: inline-flex;
  align-items: center;
  color: var(--c-ink-2);
}
.v2-field-error {
  margin: var(--s-sm) 0 0;
  color: var(--c-alarm);
  font-size: var(--t-micro);
}
.v2-scale-exact-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-sm);
  margin-top: var(--s-md);
}
.v2-scalenote {
  font-size: var(--t-micro);
  color: var(--c-brown);
  margin: 0 0 var(--s-md);
}
.v2-scalenote[hidden] { display: none; }

.v2-section { margin: 0 0 var(--s-xl); }
/* Ingredients/Method on the reading surface. They are real headings so a screen
   reader can jump between them, but they must read as the same quiet eyebrow as
   every other label — never as a second section title. Heading elements bring a
   browser margin-top that .v2-label does not reset, hence the explicit box. */
.v2-content-label { margin: var(--s-lg) 0 var(--s-xs); }
.v2-section > .v2-content-label:first-child { margin-top: 0; }
.v2-section-title {
  font-family: var(--f-serif);
  font-size: var(--t-section);
  font-weight: 400;
  margin: 0 0 var(--s-sm);
}

/* A commentable part of the recipe. It used to carry a dashed "+" in the
   reserved gutter — thirty of them down an ordinary recipe, whether or not
   anyone had ever commented (OF-09), and ten unstyled ones down a proposal in
   review until OF-46. What is left is the position context for the selection
   highlight; selecting is done from the discussion column's picker, or by
   clicking the part itself while comment mode is on. */
.v2-commentable { position: relative; }
/* While comment mode is on, the parts of the recipe are selectable. The cursor
   and a quiet ground are the only affordances: nothing moves, nothing appears,
   and with the mode off a reader's own recipe does not react to being touched. */
[data-comment-mode='true'] .v2-commentable { cursor: pointer; }
[data-comment-mode='true'] .v2-commentable:hover { background: var(--c-sink); }
/* Selection is green. DESIGN.md gives green selection, focus and the approved
   state, and yellow caution and markers — a selected row is neither a warning
   nor a marker, and this was the only place the two were swapped. */
.v2-comment-target {
  background: var(--c-green-tint);
  box-shadow: 0 0 0 3px var(--c-green-tint);
}

/* OF-40 block-level links. A linked block is the SAME component as an owned one
   — heading, ingredients, method — so it gets no rule of its own for looking
   linked. What it does get is a heading link that is a real target: DESIGN.md
   requires at least 44px even inline, achieved with padding and a compensating
   negative margin so the reading rhythm is unchanged. WCAG 2.2 would exempt an
   inline link; this does not, because the link is the only marker there is. */
.v2-block-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  margin: calc(var(--target) / -2 + 0.5em) 0;
  color: inherit;
}
/* The ten-block expand budget. Past ten blocks a linked recipe opens closed, so
   a long composition can be read as a sequence of names first. Present only on
   documents over the budget — below it a linked block is not wrapped at all, and
   is the same component as an owned one down to the markup. */
.v2-block-disclosure { margin-bottom: var(--s-md); }
.v2-block-summary {
  display: flex;
  align-items: center;
  gap: var(--s-xs);
  min-height: var(--target);
  color: var(--c-ink-2);
  font-family: var(--f-sans);
  font-size: var(--t-meta);
  cursor: pointer;
}
.v2-block-summary:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-block-summary::-webkit-details-marker { display: none; }
.v2-block-summary::marker { content: ''; }
.v2-block-chevron {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 160ms cubic-bezier(.16, 1, .3, 1);
}
.v2-block-disclosure[open] > .v2-block-summary .v2-block-chevron { transform: rotate(180deg); }
/* The first child inside an open disclosure already carries the block's own
   spacing; a second top margin here would push it away from its heading. */
.v2-block-disclosure[open] > .v2-block-summary { margin-bottom: var(--s-sm); }

/* Said only when scaling the child would have been dishonest (§4.1). */
.v2-block-aswritten {
  margin: 0 0 var(--s-md);
  color: var(--c-ink-2);
  font-family: var(--f-sans);
  font-size: var(--t-meta);
}

/* Comment mode (OF-46): one named control with a pressed state, in place of a
   "+" on every commentable row. The hint states which way the mode is set,
   because a pressed state alone is a colour, and colour never speaks alone. */
.v2-commentbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-sm);
}
.v2-commentbar[hidden] { display: none; }
.v2-commentbar .v2-meta { margin: 0; }
.v2-commentmode[aria-pressed='true'] {
  background: var(--c-green);
  border-color: var(--c-green);
  color: var(--c-surface);
}
/* On review it is a bar of its own above the workspace; on the recipe page it
   rides in the editorial action row, which already sets its own spacing. */
.v2-review-commentbar { margin: var(--s-lg) 0 var(--s-md); }

/* Ingredients: amount right-aligned and tabular so a column of numbers reads as
   a column. Free-text amounts print verbatim. */
.v2-ing { border-top: 1px solid var(--c-hairline); }
.v2-ingrow {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: var(--s-md);
  padding: var(--s-sm) 0;
  border-bottom: 1px solid var(--c-hairline);
  align-items: baseline;
}
.v2-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-small);
  color: var(--c-ink-2);
}
.v2-qty-none { color: var(--c-muted); }
.v2-ingname { font-family: var(--f-serif); font-size: var(--t-body); min-width: 0; }

/* A linked recipe is another recipe used here — a relationship, not a type.
   It used to get a green-tinted row and an uppercase LINKED RECIPE badge, which
   is precisely a type: the row stopped looking like the ingredient it is and
   started looking like a card that had landed in the list. The name carries the
   link, one quiet line carries the version, and the row keeps the rhythm of
   every other row (OF-05). */
/* The name is now the ONLY way into the linked recipe, so it has to be a real
   target: 44px, not the 24px an inline link inherits from its line box
   (DESIGN.md rule 6). inline-flex rather than block, so a long name still wraps
   with the rest of the row. */
.v2-linked-name {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  color: var(--c-ink);
  text-decoration-color: var(--c-green);
}
.v2-linked-name:hover { color: var(--c-green); }
.v2-linked-name:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }

/* ----------------------------------------------------------------------
   The block minimap (`.dotnav` in the approved mockup, never built until now).
   One dash per block, sticky beside the reading column. Only the LABELS
   expand on hover or focus — never the targets. That is what keeps it inside
   DESIGN.md rule 4, "no function that appears only on hover".
   ---------------------------------------------------------------------- */
.v2-dotnav {
  float: right;
  position: sticky;
  top: var(--s-md);
  z-index: 12;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  margin: 0 calc(-1 * var(--s-sm)) var(--s-md) var(--s-lg);
  padding: var(--s-xs);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
}
.v2-dotnav[hidden] { display: none; }
.v2-dotnav:hover,
.v2-dotnav:focus-within {
  background: var(--c-surface);
  border-color: var(--c-hairline);
  box-shadow: 0 10px 30px rgba(23, 21, 15, 0.10);
}
.v2-dot,
.v2-dot-talk {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-sm);
  /* DESIGN.md rule 6 is stated without exception, so the target is 44px even
     though the mark inside it is a 2px dash. The mockup's own 30px row is the
     one place its rules disagree with each other. */
  min-height: var(--target);
  min-width: var(--target);
  padding: 0 var(--s-xs);
  background: none;
  border: 0;
  border-radius: var(--r-xs);
  color: var(--c-ink-2);
  font: inherit;
  cursor: pointer;
}
.v2-dot:hover, .v2-dot-talk:hover { background: var(--c-sink); }
.v2-dot:focus-visible,
.v2-dot-talk:focus-visible { outline: 2px solid var(--c-green); outline-offset: -2px; }
.v2-dot i {
  display: block;
  width: 16px;
  height: 2px;
  border-radius: 2px;
  background: var(--c-hairline-strong);
  flex: 0 0 auto;
}
.v2-dot[aria-current] i { width: 22px; background: var(--c-ink); }
.v2-dl {
  font-size: var(--t-small);
  white-space: nowrap;
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  order: -1;
  /* Only the fade is animated. The mockup transitions `max-width` as well, but
     that relayouts the sticky column on every frame for as long as the pointer
     rests on it — and the width change is what reserves the space, so it has to
     be instant anyway for the labels not to lag behind their own box. */
  transition: opacity 140ms ease;
}
.v2-dotnav:hover .v2-dl,
.v2-dotnav:focus-within .v2-dl { max-width: 14rem; opacity: 1; }
.v2-dot[aria-current] .v2-dl { color: var(--c-green); font-weight: 600; }
.v2-dot-talk {
  border-bottom: 1px solid transparent;
  margin-bottom: var(--s-xs);
}
.v2-dotnav:hover .v2-dot-talk,
.v2-dotnav:focus-within .v2-dot-talk { border-bottom-color: var(--c-hairline); }
.v2-dot-count {
  font-size: var(--t-micro);
  font-weight: 700;
  background: var(--c-green);
  color: var(--c-surface);
  border-radius: 999px;
  padding: 0 var(--s-xs);
  font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) { .v2-dl { transition: none; } }

/* The narrow replacement: a jump button opening a real dialog. The minimap is
   hidden below 700px on purpose — a right-floating column has nowhere to go in
   a phone's reading width, and squeezing it there is how its comments entry
   became 0x0 and a reviewer could not open a blocking thread (BEFUNDE §7). */
.v2-jumpbtn { display: none; }
.v2-jump-sheet[hidden] { display: none; }
.v2-jump-item {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--target);
  padding: var(--s-sm);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--c-hairline);
  color: var(--c-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.v2-jump-item:hover { background: var(--c-sink); }
.v2-jump-item:focus-visible { outline: 2px solid var(--c-green); outline-offset: -2px; }

/* Provenance links on the recipe are targets too. They rendered at 16px — the
   line-box height of micro text — and no suite had seen it, because until this
   milestone's fixtures no development recipe carried a citation at all. Scoped
   to the reading column: the library card's identity rows are plain text. */
.v2-reading .v2-idlines a {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
}
.v2-linked-note {
  font-family: var(--f-sans);
  font-size: var(--t-micro);
  color: var(--c-muted);
  display: block;
}

.v2-warn-inline { color: var(--c-alarm); }
.v2-info-inline { color: var(--c-info); }

.v2-steps { counter-reset: v2step; list-style: none; margin: 0; padding: 0; }
.v2-step {
  counter-increment: v2step;
  display: grid;
  grid-template-columns: 2.2rem minmax(0, 1fr);
  gap: var(--s-md);
  padding: var(--s-sm) 0;
}
.v2-step::before {
  content: '0' counter(v2step);
  font-family: var(--f-sans);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  font-weight: 600;
  color: var(--c-brown);
  padding-top: 4px;
}
/* Prose measure: recipe method is read, not scanned. */
.v2-steptext {
  font-family: var(--f-serif);
  font-size: var(--t-body);
  line-height: 1.62;
  max-width: 68ch;
  margin: 0;
}

.v2-warnlist { margin: var(--s-xs) 0 0; padding-left: var(--s-md); font-size: var(--t-small); }
.v2-muted { color: var(--c-muted); font-size: var(--t-small); }

.v2-composed { margin-top: var(--s-xl); }
.v2-tree { list-style: none; margin: 0; padding: 0; }
.v2-tree .v2-tree { padding-left: var(--s-md); border-left: 1px solid var(--c-hairline); }
.v2-tree-item { padding: var(--s-sm) 0; border-bottom: 1px solid var(--c-hairline); }
.v2-tree-title { font-family: var(--f-serif); font-size: var(--t-body); }
.v2-tree-factor { font-variant-numeric: tabular-nums; font-size: var(--t-micro); color: var(--c-muted); }
.v2-tree-ings { font-size: var(--t-micro); color: var(--c-muted); margin: var(--s-xs) 0 0; padding-left: var(--s-md); }
.v2-tree-broken { color: var(--c-alarm); }

/* Personal writing is recessed and physically separate from both recipe prose
   and shared discussion. It never masquerades as a revision field. */
.v2-personal-note,
.v2-version-history {
  margin-top: var(--s-xl);
  padding-top: var(--s-lg);
  border-top: 1px solid var(--c-hairline-strong);
}
.v2-personal-note {
  padding: var(--s-lg);
  background: var(--c-sink);
}
.v2-collab-heading,
.v2-note-summary,
.v2-history-row,
.v2-discussion-head,
.v2-thread-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-md);
}
.v2-collab-heading .v2-section-title,
.v2-collab-heading .v2-meta { margin-bottom: 0; }
.v2-note-body {
  font-family: var(--f-serif);
  line-height: 1.62;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.v2-note-meta,
.v2-thread-meta,
.v2-history-meta {
  color: var(--c-muted);
  font-size: var(--t-meta);
  font-variant-numeric: tabular-nums;
}
.v2-personal-note .v2-field { margin-top: var(--s-md); }
.v2-note-actions { display: flex; gap: var(--s-sm); flex-wrap: wrap; }
.v2-note-delete { color: var(--c-alarm); }

.v2-version-history details > summary {
  min-height: var(--target);
  display: flex;
  align-items: center;
  cursor: pointer;
  font-family: var(--f-serif);
  font-size: var(--t-section);
}
.v2-version-history details > summary::before {
  content: '\203A';
  width: var(--s-lg);
  font-family: var(--f-sans);
  color: var(--c-brown);
}
.v2-version-history details[open] > summary::before { transform: rotate(90deg); }
.v2-version-history-state { margin-top: var(--s-md); }
.v2-history-list { list-style: none; margin: var(--s-md) 0 0; padding: 0; }
.v2-history-row {
  flex-wrap: wrap;
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-history-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  color: var(--c-ink);
  font-weight: 600;
}
.v2-history-link[aria-current='page'] { color: var(--c-green); }
/* The version and its status sat flush against each other, so "Version 12"
   and "Approved" read as one run of text (OF-20). They are two facts. */
.v2-history-row > div {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-xs) var(--s-sm);
}
.v2-history-row .v2-history-meta { flex: 1 0 100%; margin: 0; }
.v2-history-events { flex: 1 0 100%; margin-top: var(--s-sm); }
.v2-history-event {
  padding: var(--s-sm) 0;
  border-top: 1px solid var(--c-hairline);
}
.v2-history-changes { margin: var(--s-xs) 0 0; padding-left: var(--s-md); font-size: var(--t-meta); }
/* Which block the change touched, said before the change itself. Quiet: it is
   the address, not the news. */
.v2-history-where {
  display: inline-block;
  margin-right: var(--s-xs);
  padding: 0 var(--s-xs);
  border-radius: var(--r-xs);
  background: var(--c-sink);
  color: var(--c-ink-2);
  font-family: var(--f-sans);
  font-size: var(--t-micro);
}

/* Desktop discussion is a persistent complementary region, never a modal. */
.v2-discussion {
  position: sticky;
  top: var(--s-lg);
  max-height: calc(100vh - 2 * var(--s-lg));
  overflow: auto;
  padding: var(--s-lg);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
}
.v2-discussion[hidden] { display: none; }
.v2-discussion-head {
  padding-bottom: var(--s-sm);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-discussion-head .v2-section-title,
.v2-discussion-head .v2-meta { margin-bottom: 0; }
.v2-discussion-close { display: grid; flex: 0 0 var(--target); }
.v2-discussion-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s-sm);
  margin-bottom: var(--s-sm);
}
.v2-discussion-filter .v2-select { flex: 1 1 10rem; min-width: 0; }
.v2-discussion-filter .v2-label { width: 100%; margin-bottom: 0; }
.v2-discussion-filter .v2-btn[hidden] { display: none; }
.v2-discussion-anchor {
  margin: var(--s-md) 0;
  color: var(--c-brown);
  font-size: var(--t-meta);
  font-weight: 600;
}
.v2-thread {
  padding: var(--s-md) 0;
  border-top: 1px solid var(--c-hairline-strong);
}
.v2-thread-required { background: var(--c-yellow-tint); padding: var(--s-md); }
.v2-thread-label { font-weight: 600; }
.v2-thread-kind,
.v2-thread-status {
  display: block;
  font-size: var(--t-micro);
  color: var(--c-muted);
}
.v2-thread-required .v2-thread-kind { color: var(--c-yellow-ink); font-weight: 600; }
.v2-thread-message { margin-top: var(--s-md); }
.v2-thread-message p { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.v2-thread-system { color: var(--c-muted); font-size: var(--t-meta); }
.v2-thread-actions { display: flex; gap: var(--s-sm); flex-wrap: wrap; margin-top: var(--s-md); }
.v2-thread-reply { margin-top: var(--s-md); }
/* OF-41. Discussion that belongs to a linked recipe, named here and answered
   there. A separate region, not a thread: it must never look like something the
   reader could reply to or resolve from this page. */
.v2-linked-talk {
  padding-top: var(--s-lg);
  margin-top: var(--s-lg);
  border-top: 1px solid var(--c-hairline);
}
.v2-linked-talk[hidden] { display: none; }
.v2-linked-talk .v2-label { margin-bottom: var(--s-xs); }
.v2-linked-talk .v2-meta { margin-bottom: var(--s-sm); }
.v2-linked-talk-list { list-style: none; margin: 0; padding: 0; }
/* Name over count, never "name · count" on one line. The column is narrow and a
   recipe title is long, so the middot spent most of its life dangling at the end
   of a wrapped first line. Two lines say the same thing and always fit. */
.v2-linked-talk-link {
  display: grid;
  align-content: center;
  gap: 2px;
  min-height: var(--target);
  padding: var(--s-xs) 0;
  color: var(--c-ink);
  text-decoration: none;
}
.v2-linked-talk-name { font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
.v2-linked-talk-count { color: var(--c-muted); font-size: var(--t-meta); }
.v2-linked-talk-link:focus-visible { outline: 2px solid var(--c-green); outline-offset: 3px; }
.v2-linked-talk-status:not(:empty) { margin-top: var(--s-md); }

.v2-comment-form {
  padding-top: var(--s-lg);
  margin-top: var(--s-lg);
  border-top: 1px solid var(--c-hairline-strong);
}
.v2-required-choice { margin-bottom: var(--s-md); }

.v2-usedin {
  margin-top: var(--s-xl);
  padding-top: var(--s-lg);
  border-top: 1px solid var(--c-hairline-strong);
}
.v2-usedin[hidden] { display: none; }
.v2-usedin-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-md);
  margin-bottom: var(--s-md);
}
.v2-usedin-head .v2-section-title,
.v2-usedin-head .v2-meta { margin-bottom: 0; }
.v2-usedin-item {
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-usedin-link {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr);
  gap: var(--s-md);
  color: inherit;
  text-decoration: none;
}
.v2-usedin-link:hover .v2-usedin-name { text-decoration: underline; text-underline-offset: 3px; }
.v2-usedin-link:focus-visible { outline: 2px solid var(--c-green); outline-offset: 3px; }
.v2-usedin-cover,
.v2-usedin-plate {
  display: grid;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  place-items: center;
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
}
.v2-usedin-plate {
  padding: var(--s-sm);
  font-family: var(--f-serif);
  text-align: center;
}
.v2-usedin-name {
  display: block;
  margin-bottom: var(--s-sm);
  font-family: var(--f-serif);
  font-size: var(--t-section);
  line-height: 1.3;
}
.v2-usedin-occurrences {
  margin: var(--s-sm) 0 0 calc(10rem + var(--s-md));
  padding-left: var(--s-md);
  color: var(--c-muted);
  font-size: var(--t-small);
}

@media (max-width: 700px) {
  /* The gutter narrows but is never removed: it is the anchor column. */
  .v2-reading,
  .v2-reading[data-discussion-open='true'] {
    grid-template-columns: minmax(0, 1fr) var(--s-gutter);
    max-width: 100%;
  }
  .v2-ingrow { grid-template-columns: 5.5rem minmax(0, 1fr); gap: var(--s-sm); }
  /* The minimap gives way to the jump button. Both are governed here so the
     two can never be on screen together, or both be missing. */
  .v2-dotnav { display: none; }
  .v2-jumpbtn { display: inline-flex; position: sticky; top: var(--s-sm); z-index: 13; }
  .v2-jumpbtn[hidden] { display: none; }
  .v2-scalefield { flex: 1 1 100%; max-width: none; }
  .v2-usedin-link { grid-template-columns: 7rem minmax(0, 1fr); gap: var(--s-sm); }
  .v2-usedin-occurrences { margin-left: calc(7rem + var(--s-sm)); }
  .v2-personal-note { padding: var(--s-md); }
  .v2-note-summary,
  .v2-history-row { flex-direction: column; }
  .v2-note-summary .v2-btn,
  .v2-history-row .v2-btn { width: 100%; }

  /* The same discussion region becomes a sheet only after an anchor or the
     discussion summary is activated. V2States supplies modal semantics, inert
     background, focus containment, Escape and focus restoration. */
  .v2-reading > .v2-discussion {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    z-index: 60;
    grid-column: auto;
    grid-row: auto;
    width: 100%;
    max-height: 72vh;
    overflow: auto;
    border-width: 1px 0 0;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    box-shadow: 0 -12px 40px rgb(23 21 15 / 22%);
    padding: var(--s-md) var(--s-md) calc(var(--s-md) + env(safe-area-inset-bottom));
  }
  body.v2-discussion-modal-open { overflow: clip; }
}

/* ======================================================================
   Workflow action bars

   Desktop keeps the complete server-provided action set in view. At phone
   width the page script moves exactly one real action into the primary slot and
   every remaining real action into a bottom sheet. Nothing is duplicated and
   nothing permission-sensitive is inferred by CSS.
   ====================================================================== */
.v2-actionbar {
  display: flex;
  gap: var(--s-sm);
  align-items: center;
  flex-wrap: wrap;
  padding: var(--s-md) 0;
  margin-bottom: var(--s-xl);
  border-top: 1px solid var(--c-hairline-strong);
  position: sticky;
  z-index: 20;
  bottom: 0;
  background: var(--c-ground);
}
.v2-actionbar-primary,
.v2-actionbar-secondary {
  display: contents;
}
.v2-actionbar-secondary.v2-actions { margin: 0; }
.v2-actionbar-more { display: none; }
.v2-actionbar-state {
  display: inline-flex;
  align-items: center;
  color: var(--c-muted);
  font-size: var(--t-meta);
}
.v2-action-sheet-items > .v2-btn {
  display: flex;
  width: 100%;
  min-height: 52px;
  justify-content: flex-start;
  border: 0;
  border-bottom: 1px solid var(--c-hairline);
  border-radius: 0;
  background: transparent;
  color: var(--c-ink);
  font-size: var(--t-body);
  text-align: left;
}
.v2-action-sheet-items > .v2-btn-primary {
  color: var(--c-green);
  font-weight: 600;
}
.v2-action-sheet-items > .v2-btn[hidden] { display: none; }
.v2-action-sheet-items > .v2-cancel { margin-left: 0; }
.v2-sheet-close { flex: 0 0 auto; }

/* ======================================================================
   Editor (R1 scope: copy, states and terminology. The outline workspace
   with single-section editing is R6.)
   ====================================================================== */
.v2-cancel { margin-left: auto; }

/* Save state: a dot plus a word. The dot is never the only signal. */
.v2-savestate {
  display: inline-flex;
  align-items: center;
  gap: var(--s-xs);
  font-size: var(--t-meta);
  color: var(--c-muted);
}
.v2-savedot { width: 8px; height: 8px; border-radius: 50%; background: var(--c-hairline-strong); }
.v2-savestate-dirty .v2-savedot { background: var(--c-yellow); }
.v2-savestate-saving .v2-savedot { background: var(--c-info); }
.v2-savestate-saved .v2-savedot { background: var(--c-green); }
.v2-savestate-failed .v2-savedot { background: var(--c-alarm); }
.v2-savestate-conflict .v2-savedot { background: var(--c-alarm); }
.v2-savetime { color: var(--c-muted); font-variant-numeric: tabular-nums; }

.v2-structure-actions {
  display: flex;
  gap: var(--s-md);
  align-items: flex-start;
  flex-wrap: wrap;
  margin: var(--s-md) 0 var(--s-xl);
}

/* A catalog match is ingredient identity, not a recipe link. It reads as a quiet
   note rather than a badge, so it cannot be mistaken for composition. */
.v2-catmatch {
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  flex-wrap: wrap;
  margin-top: var(--s-xs);
}
.v2-catmatch-name { font-size: var(--t-micro); color: var(--c-muted); }
.v2-btn-sm { min-height: 36px; padding: var(--s-xs) var(--s-sm); font-size: var(--t-micro); }
@media (max-width: 700px) {
  /* No exception for compact controls on a phone. */
  .v2-btn-sm { min-height: var(--target); }

  body.v2-has-actionbar #v2-app {
    padding-bottom: calc(
      var(--v2-bottomnav-block) + var(--v2-actionbar-height, 0px)
    );
  }

  /* The fixed navigation owns the safe area. Workflow actions sit immediately
   * above it; the measured page inset keeps the document's last control clear. */
  .v2-actionbar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s-sm);
    padding: var(--s-sm) var(--s-md);
    margin: 0;
    border-top: 1px solid var(--c-hairline-strong);
    background: var(--c-ground);
  }
  body.v2-has-actionbar .v2-actionbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: var(--v2-bottomnav-block);
    z-index: 30;
  }
  .v2-actionbar-state,
  .v2-savestate {
    grid-column: 1 / -1;
    min-width: 0;
  }
  .v2-actionbar-primary { display: contents; }
  .v2-actionbar-secondary { display: none; }
  .v2-actionbar-primary > .v2-btn {
    min-width: 0;
    width: 100%;
  }
  .v2-actionbar-more {
    display: inline-flex;
    white-space: nowrap;
  }
  .v2-actionbar-more[hidden] { display: none; }
  .v2-actionbar[data-no-actions] {
    position: static;
    display: block;
    padding-left: 0;
    padding-right: 0;
  }
  .v2-actionbar[data-no-actions] .v2-actionbar-state {
    display: none;
  }
  .v2-actionbar[data-no-actions] .v2-actionbar-secondary {
    display: block;
  }
  .v2-action-sheet {
    z-index: 60;
  }
}

/* R1 requires 44x44 on every control at phone width, with no exception for
   "compact". The editor still carries legacy Bootstrap controls for tags,
   sources and the cover — those surfaces are restyled in later milestones, but
   the target rule applies now regardless of which layer drew the button. */
@media (max-width: 700px) {
  #v2-main .btn,
  #v2-main .btn-sm,
  #v2-main .btn-close,
  #v2-main .form-control,
  #v2-main .form-select { min-height: var(--target); }
  /* Width matters as much as height: a row-delete "x" was 27px wide at 44 tall,
     which is still a miss in a kitchen. Icon-only controls get both. */
  #v2-main .btn-close,
  #v2-main .btn-sm.btn-outline-danger { min-width: var(--target); }
}

/* ---- Review -------------------------------------------------------------
   The review queue is a reading list, not a dashboard of cards. Each row is
   one submission: its state, its title, what it contains. State is carried by
   the notation type — a word, set in the state colour — because a coloured dot
   or bar alone is not available to everyone reading it.
*/
.v2-sublist { list-style: none; margin: 0; padding: 0; }
.v2-sublist > li {
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-sublist > li:first-child { border-top: 1px solid var(--c-hairline-strong); }

.v2-queuerow {
  display: grid;
  grid-template-columns: var(--target) minmax(0, 1fr);
  gap: var(--s-sm);
  align-items: start;
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
  color: inherit;
  text-decoration: none;
  min-height: var(--target);
}
.v2-queuerow:first-child { border-top: 1px solid var(--c-hairline-strong); }
.v2-queue-link { color: inherit; text-decoration: none; min-width: 0; }
.v2-queue-link:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-queue-select {
  display: grid;
  width: var(--target);
  height: var(--target);
  place-items: center;
  cursor: pointer;
}
.v2-queue-select input { width: 1.15rem; height: 1.15rem; accent-color: var(--c-green); }

/* The reason the inert checkboxes are inert (OF-48). A disabled control never
   ships without a reason; 29 of 32 rows shipped with one and said nothing. The
   line is quiet but never hidden — DESIGN.md: don't hide a disabled reason to
   save space — and it sits directly above the rows it explains. */
.v2-selecthint {
  margin: calc(-1 * var(--s-md)) 0 var(--s-md);
  color: var(--c-muted);
  font-size: var(--t-meta);
  line-height: 1.5;
  max-width: 68ch;
}
.v2-selecthint[hidden] { display: none; }

/* Disabled must look disabled, and opacity alone is not enough (DESIGN.md). */
.v2-queue-select input:disabled { accent-color: var(--c-muted); cursor: not-allowed; }
.v2-queue-select:has(input:disabled) { cursor: not-allowed; }
.v2-queuerow:hover /* Finished imports fold away rather than disappearing: an accepted candidate is
   the only record of the decisions taken during that import. */
.v2-importhistory { border-top: 1px solid var(--c-hairline-strong); }
.v2-importhistory-summary {
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  min-height: var(--target);
  font-size: var(--t-small);
  color: var(--c-ink-2);
  cursor: pointer;
}
/* `display: flex` on a <summary> suppresses the default disclosure triangle, so
   the control would look like a line of static text. The marker is drawn back
   explicitly and rotates with the open state. */
.v2-importhistory-summary::-webkit-details-marker { display: none; }
.v2-importhistory-summary::before {
  content: '';
  flex: 0 0 auto;
  width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 120ms ease;
}
.v2-importhistory[open] > .v2-importhistory-summary::before { transform: rotate(90deg); }
.v2-importhistory-summary:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }

.v2-queuetitle { text-decoration: underline; }
/* The first column is reserved for a 44px control — the review queue puts its
   batch-select checkbox there. On the import and menu queues there is no such
   control, and the STATE went into that slot instead: "ACCEPTED" is far wider
   than 44px, so it ran into the title beside it and rendered as
   "ACCEPTEDTRASH COOKING OF FISH SCALES AND LIVERS".
   Widening the column for everyone would have ragged the review queue's
   titles, and sizing it to content ragged them against each other, since
   "ACCEPTED" and "OPEN" differ in width. So the column is dropped only where
   nothing needs it, and the row reads state -> title -> meta, the same order as
   a library card. */
.v2-queuerow:not(:has(.v2-queue-select)) {
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
}
.v2-queuerow:not(:has(.v2-queue-select)) .v2-notation { margin-top: 0; }
.v2-queuetitle {
  display: block;
  font-family: var(--f-serif);
  font-size: var(--t-lead);
  line-height: 1.25;
  margin: var(--s-xs) 0;
}
.v2-queuemeta {
  display: block;
  font-size: var(--t-meta);
  color: var(--c-muted);
}
/* "Needs me" is the one thing a reviewer scans for, so it is a word, not a dot,
   and it sits on the same line as the state it qualifies. */
.v2-needsme {
  display: inline-block;
  margin-left: var(--s-sm);
  font-size: var(--t-eyebrow);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--c-green);
}

.v2-batchbar {
  position: sticky;
  bottom: var(--s-md);
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  flex-wrap: wrap;
  margin-top: var(--s-lg);
  padding: var(--s-md);
  border: 1px solid var(--c-hairline-strong);
  background: var(--c-ground);
  box-shadow: 0 8px 28px rgb(23 21 15 / 12%);
}
.v2-batchbar[hidden] { display: none; }
.v2-batchbar > .v2-actions { margin: 0; }
.v2-batch-sheet { max-width: 42rem; left: 50%; right: auto; width: min(100%, 42rem); transform: translateX(-50%); }
.v2-batch-sheet .v2-sublist { margin: var(--s-md) 0; }

/* One submission */
.v2-review-main { max-width: none; }
.v2-review-items { margin: var(--s-lg) 0; }
.v2-review-itemnav { display: flex; gap: var(--s-sm); overflow-x: auto; padding-bottom: var(--s-xs); }
.v2-review-item-choice { display: flex; flex: 0 0 auto; align-items: stretch; }
.v2-review-item-choice > button {
  display: grid;
  gap: 2px;
  min-width: 12rem;
  text-align: left;
}
.v2-review-item-choice > button[aria-current='true'] {
  border-color: var(--c-green);
  background: var(--c-green-tint);
}
.v2-review-workspace {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(18rem, 22rem);
  gap: calc(var(--s-gutter) + var(--s-md));
  align-items: start;
}
.v2-review-document { min-width: 0; max-width: 68rem; outline: none; }
.v2-review-document-head {
  margin-bottom: var(--s-xl);
  padding-bottom: var(--s-lg);
  border-bottom: 1px solid var(--c-hairline-strong);
}
.v2-review-versionline {
  display: flex;
  gap: var(--s-md);
  justify-content: space-between;
  align-items: center;
  min-height: var(--target);
  color: var(--c-muted);
  font-size: var(--t-meta);
}
.v2-review-previous { display: inline-flex; align-items: center; min-height: var(--target); color: var(--c-green); }
.v2-review-output,
.v2-review-sources { margin-top: var(--s-md); }
/* Reviewing a translation: each source string above the sentence proposed for
 * it. Two stacked lines rather than two columns, because at 390px a side-by-side
 * pair of full sentences leaves neither readable — and both are prose, so both
 * are set in the serif. The source is quieter: the proposal is what is under
 * review. */
.v2-trrow {
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-trrow .v2-label { margin-bottom: var(--s-xs); }
.v2-trsource,
.v2-trtarget {
  font-family: var(--f-serif);
  font-size: var(--t-body);
  line-height: 1.5;
  margin: 0;
  max-width: 68ch;
}
.v2-trsource { color: var(--c-muted); }
.v2-trtarget { color: var(--c-ink); margin-top: var(--s-xs); }
/* An untranslated required field is a fact about the proposal, not an error in
 * the page: it is stated in words by the row and counted by the server's own
 * completeness line, which is what actually blocks Ready and Approve. */
.v2-trrow-missing { border-left: 0; }

/* Authoring a translation. Same stacked shape as reviewing one — source above
 * the line that replaces it — so what an author writes and what a reviewer
 * reads are laid out identically. */
.v2-trfield {
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-trfield .v2-label { margin-bottom: var(--s-xs); }
.v2-trfield .v2-trsource { margin-bottom: var(--s-sm); }
.v2-trfield .v2-input { width: 100%; }
.v2-translate-shell .v2-section-note { max-width: 68ch; }
/* No width override: `.v2-dialog` already sets `width`, so a max-width here
 * would do nothing. The consequence line reserves its height so choosing a
 * language does not resize the dialog under the pointer. */
.v2-translate-dialog .v2-meta { min-height: 2.6em; margin-top: var(--s-sm); }

.v2-trrow-missing .v2-trtarget { font-family: var(--f-sans); font-size: var(--t-meta); }

.v2-review-facts { display: grid; gap: var(--s-xs); margin-top: var(--s-md); }
.v2-review-fact {
  display: grid;
  grid-template-columns: minmax(8rem, 1fr) minmax(0, 2fr);
  gap: var(--s-sm);
  min-height: var(--target);
  align-items: center;
  font-size: var(--t-meta);
}
.v2-review-fact > span { color: var(--c-muted); }
.v2-review-notes { margin-top: var(--s-md); }
.v2-review-notes p { margin: var(--s-xs) 0 0; white-space: pre-wrap; }
.v2-review-tags { display: flex; gap: var(--s-xs); flex-wrap: wrap; }
.v2-review-tag {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  padding: 0 var(--s-sm);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-pill);
}
.v2-review-sources ul { margin: var(--s-xs) 0 0; padding-left: var(--s-lg); }
.v2-review-gallery { margin-bottom: var(--s-xl); }
.v2-review-gallery > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: var(--s-md); }
.v2-review-gallery img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; }
.v2-review-section-head { margin-bottom: var(--s-sm); }
.v2-review-ingredients { border-top: 1px solid var(--c-hairline); }
.v2-review-ingredient {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: var(--s-md);
  padding: var(--s-sm) 0;
  border-bottom: 1px solid var(--c-hairline);
  font-family: var(--f-serif);
}
.v2-review-steps { margin: var(--s-md) 0 0; padding-left: 2.2rem; }
.v2-review-steps > li { padding: var(--s-sm) 0; font-family: var(--f-serif); line-height: 1.62; }
.v2-inline-change {
  grid-column: 1 / -1;
  margin-top: var(--s-xs);
  padding: var(--s-sm);
  background: var(--c-sink);
  font-family: var(--f-sans);
  font-size: var(--t-meta);
}
.v2-inline-change ul { margin: var(--s-xs) 0 0; padding-left: var(--s-lg); }
.v2-inline-change-added { background: var(--c-green-tint); }
.v2-inline-change-removed { background: var(--c-yellow-tint); }
.v2-review-removed { margin: var(--s-xl) 0; padding-top: var(--s-lg); border-top: 1px solid var(--c-hairline-strong); }
.v2-review-discussion { grid-column: 2; }
.v2-review-discussion-open { margin: var(--s-lg) 0; }
.v2-subrow { display: flex; flex-wrap: wrap; gap: var(--s-sm); align-items: baseline; }
/* The recipe title is the row's tap target, so the LINK carries the 44px, not
   the row padding around it. A 24px-tall link inside a comfortable row measures
   as roomy and taps as a miss. */
.v2-subrow .v2-sublink {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  font-weight: 600;
  color: var(--c-ink);
}
.v2-rolenote {
  font-size: var(--t-meta);
  color: var(--c-muted);
}
.v2-revmeta {
  font-size: var(--t-meta);
  color: var(--c-muted);
  font-variant-numeric: tabular-nums;
}

/* A diff is prose about a change, not a table. Each entry names what changed
   and shows old → new; nothing relies on colour to say which is which. */
.v2-diff { margin-bottom: var(--s-lg); }
.v2-diff-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-sm);
  align-items: baseline;
  padding-bottom: var(--s-xs);
  border-bottom: 1px solid var(--c-hairline-strong);
}
.v2-diff-title { font-family: var(--f-serif); font-size: var(--t-lead); }
.v2-diff-kind {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
}
.v2-diff-kind-added { color: var(--c-green); }
.v2-diff-kind-removed { color: var(--c-alarm); }
.v2-diff-kind-modified { color: var(--c-brown); }
.v2-diff-field { font-size: var(--t-meta); margin-top: var(--s-xs); }
.v2-diff-old { text-decoration: line-through; color: var(--c-muted); }
.v2-diff-new { font-weight: 600; }

.v2-histwhat { font-weight: 600; }
.v2-histwho, .v2-histwhen { color: var(--c-muted); font-size: var(--t-meta); }
.v2-histnote { margin: var(--s-xs) 0 0; }

.v2-note-input { min-height: calc(var(--target) * 1.6); resize: vertical; }

@media (max-width: 700px) {
  .v2-batchbar {
    bottom: calc(var(--v2-bottomnav-block) + var(--s-sm));
    align-items: stretch;
  }
  .v2-batchbar > .v2-actions { display: grid; width: 100%; }
  .v2-review-workspace { display: block; }
  .v2-review-ingredient { grid-template-columns: 5.5rem minmax(0, 1fr); gap: var(--s-sm); }
  .v2-review-workspace > .v2-review-discussion {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    z-index: 60;
    width: 100%;
    max-height: 72vh;
    overflow: auto;
    border-width: 1px 0 0;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    box-shadow: 0 -12px 40px rgb(23 21 15 / 22%);
    padding: var(--s-md) var(--s-md) calc(var(--s-md) + env(safe-area-inset-bottom));
  }
}

/* ---- Editor rhythm ------------------------------------------------------
   The editor still carries legacy Bootstrap component markup for tags,
   sources, yield and the cover; those regions are restyled in a later
   milestone. Their SPACING, however, was actively misleading: every card and
   every field group inside a card used the same `mb-3`, so the gap between
   Title and Description — two fields of one thought — read exactly as large as
   the gap between the whole yield block and the tag block. Uniform spacing is
   not neutral; it flattens the structure the reader needs to see.

   Two steps, from the token scale, so grouping carries meaning until those
   regions are rebuilt: fields within a thought sit close, blocks stand apart.
*/
.v2-editlead { margin-bottom: var(--s-lg); }
.v2-editblock {
  margin-bottom: var(--s-xl);
  padding-bottom: var(--s-xl);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-editgroup { margin-bottom: var(--s-md); }
/* The last group in a block must not add to the block's own separation. */
.v2-editblock .v2-editgroup:last-child { margin-bottom: 0; }

/* R6 component workspace. The outline is orientation, never another card: it
   stays quiet and sticky while one selected composition item owns the work area. */
.v2-editor-main { max-width: none; }
.v2-section-note { max-width: 70ch; margin: 0 0 var(--s-md); }
.v2-yield-fields {
  display: grid;
  grid-template-columns: minmax(7rem, .7fr) minmax(10rem, 1fr);
  gap: var(--s-sm);
  max-width: 34rem;
}
.v2-production-rules {
  max-width: 48rem;
  margin-top: var(--s-md);
  border-top: 1px solid var(--c-hairline);
}
.v2-production-rules summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: var(--target);
  color: var(--c-ink-2);
  font-weight: 650;
  cursor: pointer;
  list-style: none;
}
.v2-production-rules summary::-webkit-details-marker { display: none; }
.v2-production-rules summary::after {
  content: '';
  width: .55rem;
  height: .55rem;
  margin-right: var(--s-xs);
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
}
.v2-production-rules[open] summary::after { transform: rotate(225deg) translate(-2px, -1px); }
.v2-production-rules summary:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-production-rules-body { padding: var(--s-sm) 0 0; }
.v2-production-rules-body .v2-actions { margin-bottom: 0; }

.v2-structure-shell { padding-top: 0; }
.v2-structure-layout {
  display: grid;
  grid-template-columns: minmax(13rem, 17rem) minmax(0, 1fr);
  gap: clamp(var(--s-lg), 4vw, var(--s-2xl));
  align-items: start;
}
.v2-structure-outline {
  position: sticky;
  top: var(--s-lg);
  max-height: calc(100vh - var(--s-2xl));
  overflow: auto;
  padding-right: var(--s-sm);
}
.v2-outline-list { list-style: none; margin: var(--s-md) 0; padding: 0; }
.v2-outline-item { margin: 0; }
/* Linked recipe blocks share the same composition level as sections. Their
   label says what they are; indentation would wrongly imply they belong inside
   the preceding section. */
.v2-outline-item-linked { padding-left: 0; }
.v2-outline-choice {
  display: grid;
  width: 100%;
  min-height: var(--target);
  padding: var(--s-xs) var(--s-sm);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--c-ink);
  text-align: left;
  cursor: pointer;
}
.v2-outline-choice:hover { background: var(--c-sink); }
.v2-outline-choice[aria-current="true"] {
  background: var(--c-green-tint);
  color: var(--c-green);
}
.v2-outline-choice:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-outline-kind {
  color: var(--c-muted);
  font-size: var(--t-micro);
  line-height: 1.25;
}
.v2-outline-choice[aria-current="true"] .v2-outline-kind { color: inherit; }
.v2-outline-name { overflow-wrap: anywhere; font-weight: 650; }
.v2-outline-global-actions,
.v2-structure-toolbar { display: flex; flex-wrap: wrap; gap: var(--s-xs); }
.v2-outline-global-actions { flex-direction: column; align-items: stretch; }
.v2-outline-global-actions .v2-btn { justify-content: flex-start; }
.v2-structure-toolbar { margin-bottom: var(--s-xs); }
.v2-workspace-head { margin: 0 0 var(--s-md); }
.v2-workspace-head .v2-section-title { margin: var(--s-xs) 0; }
.v2-workspace-head p { margin: 0; }
.v2-structure-reason { min-height: 2.6em; margin: 0 0 var(--s-md); }
.v2-structure-mobile-nav { display: none; margin-bottom: var(--s-lg); }
.v2-structure-mobile-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-sm); }
#v2-structure-undo {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-md);
  margin: 0 0 var(--s-md);
  padding: var(--s-sm) var(--s-md);
  background: var(--c-green-tint);
  border-radius: var(--r-sm);
}
#v2-structure-undo[hidden] { display: none; }

.v2-sectioncard[data-component],
.v2-linkedcard[data-component] {
  animation: v2-component-reveal 180ms cubic-bezier(.16, 1, .3, 1);
}
@keyframes v2-component-reveal {
  from { clip-path: inset(0 0 6% 0); filter: blur(1px); opacity: .94; }
  to { clip-path: inset(0); filter: blur(0); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .v2-sectioncard[data-component], .v2-linkedcard[data-component] { animation: none; }
}

.v2-linkedcard {
  padding: var(--s-lg);
  border-top: 1px solid var(--c-hairline-strong);
  border-bottom: 1px solid var(--c-hairline-strong);
}
.v2-linkedcard-head {
  display: flex;
  align-items: start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--s-md);
  margin-bottom: var(--s-xs);
}
.v2-linkedcard-title {
  margin: var(--s-xs) 0 0;
  font-family: var(--f-serif);
  font-size: var(--t-section);
  font-weight: 400;
}
.v2-linked-preview {
  margin: var(--s-lg) 0;
  padding: var(--s-md) 0;
  border-top: 1px solid var(--c-hairline);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-linked-preview-section + .v2-linked-preview-section {
  margin-top: var(--s-lg);
  padding-top: var(--s-lg);
  border-top: 1px solid var(--c-hairline);
}
.v2-linked-preview-section h4 {
  margin: 0 0 var(--s-md);
  font-family: var(--f-serif);
  font-size: var(--t-lead);
  font-weight: 400;
}
.v2-linked-preview-section .v2-label { margin: var(--s-md) 0 var(--s-xs); }
.v2-linked-preview-section .v2-label:first-child { margin-top: 0; }
.v2-linked-preview-list { margin: 0; padding-left: 1.2em; }
.v2-linked-preview-list li { margin: var(--s-xs) 0; }
.v2-linked-preview-empty { margin: 0; }
.v2-linkedcard-fields { margin: var(--s-lg) 0 var(--s-md); }
.v2-linked-explain { margin: 0; color: var(--c-ink-2); font-size: var(--t-meta); }

/* OF-43. The way to check WHICH recipe an ingredient row links to. That row's
   name is read-only because it belongs to the other recipe, so without this an
   author has no way to confirm what they picked.

   Same rule as `.v2-linked-name` in the reading column: a link must be a real
   target, 44px rather than the 24px an inline link inherits from its line box,
   with a compensating negative margin so the row's rhythm is unchanged. */
.v2-linked-open {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  padding: 0 var(--s-sm);
  margin-left: calc(var(--s-sm) * -1);
  font-size: var(--t-meta);
  color: var(--c-ink);
  text-decoration-color: var(--c-green);
}
.v2-linked-open:hover { color: var(--c-green); }
.v2-linked-open:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }

/* C6 editable comboboxes. The input remains free text; identity is attached only
   by activating an explicit option. The popup is local to its field so a failed
   vocabulary never replaces the editor's page-level state. */
.v2-combobox { position: relative; }
.v2-combobox-control { display: grid; grid-template-columns: minmax(0, 1fr) var(--target); }
.v2-combobox-control .v2-input { border-radius: var(--r-xs) 0 0 var(--r-xs); }
.v2-combobox-toggle {
  min-width: var(--target);
  width: var(--target);
  padding: 0;
  border-left: 0;
  border-radius: 0 var(--r-xs) var(--r-xs) 0;
}
.v2-combobox-toggle::before {
  content: '';
  width: .55rem;
  height: .55rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
}
.v2-combobox-list {
  position: absolute;
  z-index: 45;
  top: calc(100% + var(--s-xs));
  left: 0;
  right: 0;
  max-height: 18rem;
  overflow: auto;
  padding: var(--s-xs);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  background: var(--c-surface);
  box-shadow: 0 10px 24px rgb(23 21 15 / 12%);
}
.v2-combobox-list[hidden] { display: none; }
.v2-combobox-option {
  display: block;
  width: 100%;
  min-height: var(--target);
  padding: var(--s-sm);
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--c-ink);
  text-align: left;
}
.v2-combobox-option:hover,
.v2-combobox-option:focus-visible { background: var(--c-green-tint); }
.v2-combobox-option strong { display: block; font-weight: 650; }
.v2-combobox-option span,
.v2-combobox-message { display: block; margin: 0; color: var(--c-muted); font-size: var(--t-micro); }
.v2-combobox-status { min-height: 1.4em; margin: var(--s-xs) 0 0; font-size: var(--t-micro); }
.v2-tag-entry {
  display: grid;
  grid-template-columns: minmax(0, 36rem) auto;
  gap: var(--s-sm);
  align-items: start;
}
.v2-tag-add-action { padding-top: 1.55rem; }
.v2-edname.v2-combobox { position: relative; }
.v2-catalog-options { top: calc(var(--target) + var(--s-xs)); }

.v2-cover-editor {
  display: grid;
  grid-template-columns: 12rem minmax(0, 1fr);
  gap: var(--s-lg);
  align-items: start;
}
.v2-cover-state {
  display: grid;
  place-items: center;
  min-height: 9rem;
  padding: var(--s-md);
  border: 1px solid var(--c-hairline-strong);
  background: var(--c-sink);
  text-align: center;
}
.v2-cover-state .v2-coverpreview { margin: 0; }
.v2-cover-state p { margin: 0; }
.v2-cover-controls { display: grid; gap: var(--s-sm); max-width: 38rem; }

.v2-dialog-head,
.v2-picker-search-row,
.v2-picker-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-sm);
}
.v2-dialog-head { align-items: start; }
.v2-picker-dialog { width: min(42rem, calc(100vw - 2 * var(--s-md))); }
.v2-picker-search { margin: var(--s-md) 0; }
.v2-picker-search-row .v2-input { flex: 1 1 auto; min-width: 0; }
.v2-picker-results { margin: var(--s-md) 0; }
.v2-picker-result {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--s-md);
  align-items: center;
  padding: var(--s-md) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-picker-result h3 { margin: 0 0 var(--s-xs); font-family: var(--f-serif); font-size: var(--t-lead); }
.v2-picker-result p { margin: 0; overflow-wrap: anywhere; }
.v2-impact-summary {
  margin: var(--s-md) 0;
  padding: var(--s-md);
  background: var(--c-sink);
  border-radius: var(--r-sm);
}
.v2-impact-summary p { margin: 0 0 var(--s-xs); }
.v2-impact-summary p:last-child { margin-bottom: 0; }
.v2-checkrow {
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  min-height: var(--target);
  margin-top: var(--s-md);
}
.v2-checkrow input { width: 1.25rem; height: 1.25rem; }

@media (max-width: 860px) {
  .v2-structure-mobile-nav { display: block; }
  .v2-structure-layout { display: block; }
  .v2-structure-outline { display: none; }
  .v2-structure-toolbar { display: none; }
}

@media (max-width: 700px) {
  .v2-editor-main { padding-left: var(--s-md); padding-right: var(--s-md); }
  .v2-yield-fields { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .v2-structure-mobile-row { grid-template-columns: minmax(0, 1fr); }
  .v2-structure-mobile-row .v2-btn { width: 100%; }
  .v2-picker-result { grid-template-columns: minmax(0, 1fr); }
  .v2-picker-result .v2-btn { width: 100%; }
  .v2-picker-pager { align-items: stretch; }
  .v2-picker-pager .v2-btn { flex: 1 1 0; }
  .v2-linkedcard { padding: var(--s-md) 0; }
  .v2-tag-entry,
  .v2-cover-editor { grid-template-columns: minmax(0, 1fr); }
  .v2-tag-add-action { padding-top: 0; }
  .v2-tag-add-action .v2-btn { width: 100%; }
  .v2-cover-state { max-width: 14rem; }
}

/* Tag and citation rows.
   These were Bootstrap `badge` + `btn-close`, which put the ONLY way to remove a
   tag behind a 16px control. The chip carries the label; the remove button is a
   full target beside it, so the reading size of the chip does not decide the
   size of the hit area. */
.v2-editchips { display: flex; gap: var(--s-xs); flex-wrap: wrap; margin-bottom: var(--s-md); }
.v2-editchip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-xs);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-pill);
  padding-left: var(--s-sm);
  font-size: var(--t-micro);
  color: var(--c-ink-2);
  min-height: var(--target);
}
/* Citations are a stacked list, not a pill row: a title with a locator is a
   line of text, and wrapping it into a pill hides where one source ends. */
.v2-editrows { display: flex; flex-direction: column; gap: var(--s-xs); }
.v2-editrow {
  display: flex;
  align-items: center;
  gap: var(--s-xs);
  border: 1px solid var(--c-hairline);
  border-radius: var(--r-sm);
  padding-left: var(--s-sm);
  min-height: var(--target);
}
.v2-editrow-text { flex: 1 1 auto; min-width: 0; font-size: var(--t-micro); color: var(--c-ink-2); }
.v2-chipremove {
  flex: 0 0 var(--target);
  min-width: var(--target);
  min-height: var(--target);
  background: none;
  border: 0;
  border-radius: var(--r-pill);
  color: var(--c-ink-2);
  font-size: var(--t-body);
  line-height: 1;
  cursor: pointer;
}
.v2-chipremove:hover { color: var(--c-brown); }
.v2-chipremove:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }

/* Section, ingredient and step rows.
   These were Bootstrap's 12-column grid, which fixed an ingredient's amount at
   3/12 of the width at EVERY size: on a phone that is a box too narrow to read
   "2 x 400 g" in, and the delete control sat in a 1/12 column well under the
   target. Amount and unit are sized to their content; the name takes the rest
   and drops to its own line when there is no room for three. */
.v2-sectioncard {
  border: 1px solid var(--c-hairline);
  border-radius: var(--r-sm);
  margin-bottom: var(--s-lg);
}
.v2-sectionhead {
  display: flex;
  gap: var(--s-sm);
  align-items: center;
  flex-wrap: wrap;
  padding: var(--s-md);
  border-bottom: 1px solid var(--c-hairline);
}
.v2-sectionhead input { flex: 1 1 12rem; min-width: 0; }
.v2-sectionbody { padding: var(--s-md); }
/* The method heading is a new thought inside the section, not another field. */
.v2-steps-label { margin-top: var(--s-lg); }

/* The editor's ingredient row is the THIRD instance of the shape defined above,
   and it joins the same rule rather than keeping its own.
   It was a flex row with `flex: 1 1 14rem` on the name and `flex-wrap: wrap`,
   which dropped the name onto its own line below roughly 30rem of available
   width — in the editor's two-column workspace, most of the time. Same symptom
   as the import form, reached by a different mechanism.
   This is CSS-only on purpose: the markup keeps its `.v2-ed*` wrappers so C6
   can restructure the editor without first unpicking a rename. */
.v2-edrow {
  display: grid;
  gap: var(--s-sm);
  align-items: start;
  margin-bottom: var(--s-sm);
  grid-template-columns:
    minmax(0, 7rem) minmax(0, 7rem) minmax(0, 1fr) var(--target);
}
.v2-edqty, .v2-edunit, .v2-edname { min-width: 0; }

/* Units are selected from the shared vocabulary, never authored as arbitrary
   text. The editor supplies the ARIA combobox behavior; this only establishes
   the local overlay and preserves the ingredient-row geometry. */
.v2-unit-combobox { position: relative; min-width: 0; }
.v2-unit-combobox > input { width: 100%; }
.v2-unit-options { width: max-content; min-width: 100%; }

.v2-steprow { display: flex; gap: var(--s-sm); align-items: flex-start; margin-bottom: var(--s-sm); }
.v2-steprow textarea { flex: 1 1 auto; min-width: 0; }
/* A method is prose. It opens at a useful writing size and grows with the
   instruction, rather than trapping a long cooking step inside a tiny scroller. */
.v2-method-input { overflow: hidden; resize: vertical; min-height: 10rem; line-height: 1.62; }

/* The cover preview is a bounded thumbnail, not a hero. */
.v2-coverpreview {
  max-width: 100%;
  max-height: 12rem;
  border: 1px solid var(--c-hairline);
  border-radius: var(--r-sm);
  margin-bottom: var(--s-md);
}

/* ---- Imports -------------------------------------------------------------
   The decision surface. Its whole job is that a reviewer never chooses blind,
   so a block shows its extracted content first and its decision second, with
   the consequence of that decision spelled out underneath the choice rather
   than hidden in a tooltip.
*/
.v2-subblock {
  border: 1px solid var(--c-hairline);
  border-radius: var(--r-sm);
  padding: var(--s-md);
  margin: 0 0 var(--s-md);
}
.v2-subblock legend { padding: 0 var(--s-xs); }

/* The three doors. Each states what it is, what it needs from you, and what it
   leaves behind — the last being the point: none of them approves anything.
   These replace four intake forms stacked on one page, which made the first
   thing a contributor met a wall of every mechanism at once. */
.v2-doors {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s-md);
  margin: var(--s-lg) 0 var(--s-xl);
}
.v2-door {
  display: grid;
  align-content: start;
  gap: var(--s-xs);
  min-height: var(--target);
  padding: var(--s-md);
  text-align: left;
  font: inherit;
  color: var(--c-ink);
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.v2-door:hover { background: var(--c-sink); }
.v2-door:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }
.v2-door-title { font-family: var(--f-serif); font-size: var(--t-section); line-height: 1.2; }
.v2-door-body { font-size: var(--t-small); color: var(--c-ink-2); }
/* The consequence, held apart from the description so it is not read as more
   marketing. */
.v2-door-foot {
  margin-top: var(--s-xs);
  font-size: var(--t-micro);
  color: var(--c-muted);
}

/* A text button that reads as the link it behaves like. */
.v2-linkbtn {
  min-height: var(--target);
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--c-green);
  cursor: pointer;
  text-decoration: underline;
}
.v2-linkbtn:focus-visible { outline: 2px solid var(--c-green); outline-offset: 2px; }

.v2-import-stages {
  display: flex;
  align-items: center;
  gap: var(--s-sm);
  margin: 0 0 var(--s-lg);
  border-bottom: 1px solid var(--c-hairline-strong);
}
.v2-import-stage {
  min-height: var(--target);
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--c-muted);
  padding: var(--s-sm) var(--s-xs);
  font: inherit;
  cursor: pointer;
}
.v2-import-stage[aria-current="step"] {
  border-bottom-color: var(--c-green);
  color: var(--c-green);
  font-weight: 650;
}
.v2-import-stage:focus-visible { outline: 2px solid var(--c-info); outline-offset: 2px; }

/* Adaptive form grid: no guessed breakpoints, no Bootstrap column arithmetic. */
.v2-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--s-md);
}
.v2-grid .v2-field { margin-bottom: 0; }
/* A button in a field row aligns to the inputs beside it, not to their labels. */
.v2-field-action { display: flex; align-items: flex-end; }

/* Page image beside the blocks read from it. */
.v2-scanlayout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-lg);
}
/* The photographed page belongs BESIDE the text read from it — that is the
   whole point of a correction stage, and it is what the approved mockup
   specifies. It was keyed to the viewport, so the pane collapsed above the
   text whenever the window was under 1000px and left a long dead column
   beneath it. Same wrong dimension as the ingredient row: what governs this
   layout is the width of the content column, not of the window. */
/* The container is the candidate view that WRAPS this grid: an element cannot
   query itself, so declaring `container-type` on `.v2-scanlayout` would have
   produced a rule that never matches. */
/* The container is the candidate view that WRAPS this grid: an element cannot
   query itself, so declaring `container-type` on `.v2-scanlayout` would have
   produced a rule that never matches.
   The threshold is what the two columns need to both stay readable — a 5fr
   image beside 7fr of text — not a guess at a device. */
#v2-one { container-type: inline-size; }
@container (min-width: 52rem) {
  .v2-scanlayout:has(.v2-scancol:not([hidden])) {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  }
}
.v2-scancol[hidden] { display: none; }
.v2-scansticky { position: sticky; top: var(--s-md); }
.v2-scanimg {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-sm);
  background: var(--c-sink);
}

.v2-block {
  border-top: 1px solid var(--c-hairline-strong);
  padding: var(--s-md) 0 var(--s-lg);
}
.v2-block-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-sm);
  align-items: baseline;
  margin-bottom: var(--s-sm);
}
.v2-block-name { font-family: var(--f-serif); font-size: var(--t-lead); }
/* Recognition confidence is a number the reviewer weighs, not a warning. It is
   never coloured as one: a low score means "read this closely", not "error". */
.v2-block-meta {
  font-size: var(--t-meta);
  color: var(--c-muted);
  font-variant-numeric: tabular-nums;
}
.v2-block-content {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--s-md);
  margin-bottom: var(--s-md);
}
.v2-block-content ul, .v2-block-content ol { margin: 0; padding-left: 1.2em; }
.v2-block-content li { font-size: var(--t-small); }
/* THE ingredient row. One rule, three surfaces: import correction, the
   manual-create form, and the editor.
   ------------------------------------------------------------------------
   The defect this replaces was the same shape in all three. The row declared
   fixed tracks — `7rem 7rem minmax(10rem, 1fr) var(--target)`, 452px that
   cannot compress — inside a column that shrinks freely. A viewport media query
   at 700px could not help, because the row's problem is the width of its
   COLUMN, not of the window: on a 14" laptop at 125-150% zoom, or simply an
   unmaximised window, the column fell to ~374px while the row still demanded
   452px and overflowed 78px into the method column beside it. Measured, not
   inferred.

   So the query is a CONTAINER query. That is not a modernism for its own sake:
   it is the only kind of query that can see the dimension that actually
   governs this layout. Anything keyed to the viewport is guessing about the
   column, and guessing is what produced the overlap. */
.v2-rowfield { container-type: inline-size; }

.v2-correction-row {
  display: grid;
  gap: var(--s-sm);
  /* `start`, not `flex-start` on a flex row: the delete control is a 44px
     target beside single-line inputs, and stretching it made it read as an
     oversized, misaligned block. It now sits on the row's first line. */
  align-items: start;
  margin-bottom: var(--s-sm);
}
.v2-correction-ingredient {
  /* minmax(0, ...) on every flexible track. A bare `7rem` refuses to shrink;
     these give way in order rather than overflowing the container. */
  grid-template-columns:
    minmax(0, 7rem) minmax(0, 7rem) minmax(0, 1fr) var(--target);
}
.v2-correction-step { grid-template-columns: minmax(0, 1fr) var(--target); }

/* Below this the three fields cannot share a line and stay readable, so the
   name takes its own. The amount must never become a box too narrow to read
   "2 x 400 g" in — that was the original Bootstrap defect this layer replaced,
   and it is not to be reintroduced by squeezing instead of wrapping.

   24rem, measured rather than guessed. The row needs 7 + 7rem for amount and
   unit, 44px for the control and three gaps: about 20rem before the name gets
   anything at all. At 24rem the name still has ~5rem, which is thin but
   legible; below it the name is better off with a line of its own. An earlier
   30rem threshold collapsed rows in a ~470px column that had room to spare,
   which defeats the point of putting them on one line. */
@container (max-width: 24rem) {
  .v2-correction-ingredient,
  .v2-edrow {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) var(--target);
  }
  .v2-correction-ingredient .v2-ingredient-name,
  .v2-edrow .v2-edname { grid-column: 1 / 3; grid-row: 2; }
  .v2-correction-ingredient .v2-btn-icon,
  .v2-edrow .v2-edaction { grid-column: 3; grid-row: 1 / 3; }
}
.v2-correction-step textarea { min-height: calc(var(--target) * 1.4); resize: vertical; }
.v2-normalization {
  display: block;
  color: var(--c-yellow-ink);
  font-size: var(--t-micro);
  margin-top: 2px;
}
.v2-scale-facts {
  margin: var(--s-sm) 0 0;
  padding-left: 1.2em;
  color: var(--c-yellow-ink);
  font-size: var(--t-meta);
}
.v2-dupe-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-sm);
  align-items: center;
  padding: var(--s-xs) 0;
}
.v2-dupe-score { font-size: var(--t-meta); color: var(--c-muted); }
/* The recipe title is the link a reviewer taps to check the suspected duplicate
   before deciding, so it carries the target itself — 24px of text in a roomy row
   measures as comfortable and taps as a miss. */
.v2-dupe-row a {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
}

/* The recipe-level check is the last decision before materialisation. Keep its
   evidence in one restrained surface: title on the left, reasons on the right,
   with the deliberate override beside the route back to correction. */
.v2-recipe-dupe-subjects {
  display: grid;
  gap: var(--s-sm);
  margin-top: var(--s-md);
}
.v2-recipe-dupe-subject {
  min-width: 0;
  padding-top: var(--s-sm);
  border-top: 1px solid color-mix(in srgb, var(--c-yellow-ink) 24%, transparent);
}
.v2-recipe-dupe-subject-title {
  margin: 0 0 var(--s-xs);
  font-family: var(--f-serif);
  font-size: var(--t-lead);
  color: var(--c-yellow-ink);
}
.v2-recipe-dupe-match {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(12rem, auto);
  gap: var(--s-sm);
  align-items: center;
  padding: var(--s-xs) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--c-yellow-ink) 14%, transparent);
}
.v2-recipe-dupe-match a {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  overflow-wrap: anywhere;
}
.v2-recipe-dupe-evidence {
  color: var(--c-yellow-ink);
  font-size: var(--t-meta);
  text-align: end;
  overflow-wrap: anywhere;
}
.v2-recipe-dupe-actions { margin-bottom: 0; }

/* An icon-only control needs its width as well as its height. Inside an adaptive
   grid the cell can narrow below the target, and a button that merely fills its
   cell then ends up 43px wide — passing a height-only check while still being a
   miss under a thumb. */
.v2-btn-icon {
  flex: 0 0 var(--target);
  min-width: var(--target);
  padding: 0;
}

/* Small inline links on this surface are still the way out of it — "to the
   review" on an accepted candidate is the only onward step there is. Meta type
   size does not license a meta-sized target. */
#v2-cand-head a,
a.v2-block-meta {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
}

@keyframes v2-import-stage-in {
  from { clip-path: inset(0 0 5% 0); filter: blur(1.5px); opacity: .92; }
  to { clip-path: inset(0); filter: blur(0); opacity: 1; }
}
#v2-components { animation: v2-import-stage-in 220ms cubic-bezier(.16, 1, .3, 1); }

@media (max-width: 700px) {
  .v2-import-stages { justify-content: space-between; }
  .v2-import-stage { flex: 1 1 0; }
  /* The ingredient row's own reflow is a container query now — see
     `.v2-correction-ingredient`. Repeating it here would key the same decision
     to the viewport, which is the dimension that could not see the problem. */
  .v2-savebar .v2-actions { display: grid; grid-template-columns: minmax(0, 1fr); }
  .v2-savebar .v2-btn { width: 100%; }
  .v2-recipe-dupe-match { grid-template-columns: minmax(0, 1fr); }
  .v2-recipe-dupe-evidence { text-align: start; }
}

.v2-joblist { list-style: none; margin: var(--s-md) 0 0; padding: 0; }
.v2-joblist > li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-sm);
  align-items: center;
  padding: var(--s-sm) 0;
  border-bottom: 1px solid var(--c-hairline);
}
.v2-jobstate {
  font-size: var(--t-eyebrow);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
}
.v2-jobstate-done { color: var(--c-green); }
.v2-jobstate-error { color: var(--c-alarm); }
.v2-jobstate-queued, .v2-jobstate-running { color: var(--c-brown); }
.v2-jobname { font-size: var(--t-small); }
.v2-joberror { font-size: var(--t-meta); color: var(--c-alarm); }
.v2-batchline { font-size: var(--t-meta); color: var(--c-ink-2); padding: var(--s-xs) 0; }

/* ---- Administration ------------------------------------------------------
   Access is read as one effective outcome. Exact packages remain available for
   deliberate editing, but recovery and account status are separate jobs rather
   than competing controls in one undifferentiated member card.
*/
.v2-person-management { display: grid; gap: var(--s-md); }
.v2-person-region {
  min-width: 0;
  border-top: 1px solid var(--c-hairline);
  padding-top: var(--s-md);
}
.v2-person-region-title {
  margin: 0 0 var(--s-xs);
  font-family: var(--f-serif);
  font-size: var(--t-section);
  font-weight: 400;
}
.v2-person-region-note {
  margin: 0 0 var(--s-sm);
  color: var(--c-muted);
  font-size: var(--t-small);
  overflow-wrap: anywhere;
}
.v2-access-summary { border-top-color: var(--c-hairline-strong); }
.v2-access-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--s-xs) var(--s-md);
  margin: 0;
  padding-left: 1.2rem;
  list-style: disc;
}
.v2-access-list li {
  min-width: 0;
  overflow-wrap: anywhere;
}
.v2-access-list li::marker { color: var(--c-green); }
.v2-access-pending,
.v2-access-redundancy {
  margin: var(--s-sm) 0 0;
  font-size: var(--t-small);
  overflow-wrap: anywhere;
}
.v2-access-pending { color: var(--c-yellow-ink); font-weight: 600; }
.v2-access-redundancy { color: var(--c-yellow-ink); }
.v2-access-disclosure > summary {
  display: flex;
  align-items: center;
  min-height: var(--target);
  cursor: pointer;
  color: var(--c-ink-2);
  font-weight: 600;
}
.v2-access-disclosure > summary:focus-visible {
  outline: 3px solid var(--c-green);
  outline-offset: 3px;
}
.v2-bundleset { border: 0; margin: 0; padding: 0; min-width: 0; }
.v2-bundle { align-items: flex-start; }
.v2-bundle label { display: block; padding: var(--s-sm) 0; }
.v2-bundle-name { display: block; font-weight: 600; }
.v2-bundle-why {
  display: block;
  font-size: var(--t-meta);
  color: var(--c-muted);
}
.v2-bundleset:disabled { opacity: .68; }
.v2-access-actions { margin-bottom: 0; }

/* ---- Dialog -------------------------------------------------------------
   Replaces window.confirm() and window.prompt(). confirm() cannot hold more
   than one sentence, so it cannot state a consequence; prompt() renders a typed
   password as clear text in browser chrome and cannot be masked at all. Focus
   containment and the inert background come from V2States.openDialog.
*/
.v2-dialog {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 80;
  width: min(28rem, calc(100vw - 2 * var(--s-md)));
  max-height: calc(100vh - 2 * var(--s-lg));
  overflow: auto;
  background: var(--c-surface);
  border: 1px solid var(--c-hairline-strong);
  border-radius: var(--r-md);
  box-shadow: 0 18px 50px rgb(23 21 15 / 26%);
  padding: var(--s-lg);
}
.v2-dialog[hidden] { display: none; }
.v2-dialog-title {
  font-family: var(--f-serif);
  font-size: var(--t-section);
  font-weight: 400;
  margin: 0 0 var(--s-sm);
}
.v2-dialog-body { margin: 0 0 var(--s-sm); }
/* The consequence sits first and reads as the point, not as fine print. */
.v2-dialog-body:first-of-type { color: var(--c-ink); font-weight: 600; }
.v2-dialog-access {
  padding-top: var(--s-sm);
  border-top: 1px solid var(--c-hairline);
}

@media (max-width: 700px) {
  /* On a phone the dialog is a sheet: reachable with a thumb, not centred in
     the middle of the screen where nothing can be reached. */
  .v2-dialog {
    top: auto;
    bottom: 0;
    left: 0;
    transform: none;
    width: 100%;
    max-width: none;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding: var(--s-md) var(--s-md) calc(var(--s-md) + env(safe-area-inset-bottom));
  }
}

/* ---- Tabs ---------------------------------------------------------------
   Seven panels of shared-library forms. The pattern carries its own state in
   aria-selected rather than in a class, so what is showing is announced and not
   merely visible; arrow keys move between tabs, because tabbing to the seventh
   panel would otherwise cross six panels of form fields.
*/
/* UX-08. Two named groups, because ordinary curation and operations that reach
   every recipe are not peers. The system group is set apart by ground and a rule
   rather than by colour alone — a warning nobody can see is not a warning, and a
   red tint would say "error" where "be careful" is meant. */
.v2-tabgroup { margin-bottom: var(--s-lg); }
.v2-tabgroup[hidden] { display: none; }
.v2-tabgroup > .v2-label { margin-bottom: 2px; }
.v2-tabgroup > .v2-muted { margin: 0 0 var(--s-sm); font-size: var(--t-meta); }
.v2-tabgroup-system {
  padding: var(--s-md);
  background: var(--c-sink);
  /* A top rule, the same way `.v2-readonly` marks a surface that behaves
     differently. Not a side accent bar: this codebase has no such pattern, and
     the group is already told apart by its ground, its heading and its note. */
  border-top: 3px solid var(--c-brown);
}
/* Each group holds its own panels, so the active panel sits under the tabs that
   opened it. With both tab rows stacked above every panel, a curation panel
   appeared below the system block and read as belonging to it. */
.v2-tabgroup [role='tabpanel'] { margin-top: var(--s-md); }
.v2-tabgroup [role='tabpanel'][hidden] { display: none; }
.v2-managesearch { margin-bottom: var(--s-sm); }
/* What a retirement reaches, between the explanation and the buttons. */
.v2-dialog-usage {
  margin: 0 0 var(--s-md);
  padding: var(--s-sm) var(--s-md);
  background: var(--c-sink);
  font-family: var(--f-sans);
  font-size: var(--t-meta);
  color: var(--c-ink-2);
}
.v2-dialog-usage-link { color: inherit; }

.v2-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-xs);
  border-bottom: 1px solid var(--c-hairline-strong);
  margin-bottom: var(--s-lg);
}
.v2-tab {
  font: inherit;
  font-size: var(--t-small);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--c-muted);
  min-height: var(--target);
  padding: 0 var(--s-md);
  cursor: pointer;
}
.v2-tab:hover { color: var(--c-ink); }
.v2-tab[aria-selected="true"] {
  color: var(--c-green);
  border-bottom-color: var(--c-green);
  font-weight: 600;
}
.v2-tab:focus-visible { outline: 2px solid var(--c-green); outline-offset: -2px; }
/* The panel itself is focusable, so moving into it after a tab keeps the reader
   where the content is. It must not draw a focus ring for a mouse click. */
[role="tabpanel"]:focus { outline: none; }
[role="tabpanel"]:focus-visible { outline: 2px solid var(--c-green); outline-offset: 4px; }

/* Form beside its list, one column when there is no room for two. */
.v2-managelayout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-xl);
}
@media (min-width: 900px) {
  .v2-managelayout { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
}

/* A compact button is compact in PADDING, never in target. Inside a wrapping row
   these squeezed to 40.58px wide at 390px — comfortable-looking, still a miss. */
.v2-btn-sm {
  flex: 0 0 auto;
  min-width: var(--target);
}
