/*
 * RecipeHub stylesheet.
 *
 * Course requirement 27 asks for five specific CSS3 capabilities. Each one is
 * marked below with the tag [CSS3 n/5] so it can be pointed at during the
 * defense rather than hunted for:
 *
 *   1. @font-face          - the Heebo Hebrew webfont, served from this project
 *   2. border-radius       - cards, buttons, inputs, avatars
 *   3. text-shadow         - the site title and section headings
 *   4. transition          - buttons, links, cards, form fields
 *   5. multiple-columns    - the feature list and long ingredient lists
 *
 * The interface is Hebrew and right-to-left throughout. Directional properties
 * use the logical forms (inline-start / inline-end) rather than left / right,
 * so the layout follows the writing direction instead of fighting it.
 */

/* ------------------------------------------------------------------------
   The @font-face declarations and the token block that used to open this file
   now live in `tokens.css`, which loads before it. They were not deleted: the
   [CSS3 1/5] @font-face proof moved there verbatim, and the legacy token names
   this file still uses (--bg, --accent, --radius, ...) are aliased onto the
   semantic layer at the bottom of that file.

   That aliasing is what lets this stylesheet keep working, unchanged, while
   each screen migrates onto the `rh-` component vocabulary - rather than a flag
   day where every screen moves at once and no regression can be bisected.
   ------------------------------------------------------------------------ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/*
 * The body rule moved to `tokens.css`, which sets `font: var(--type-body)` -
 * 17px at 1.55, not 16px at 1.65.
 *
 * It is deleted here rather than corrected, because this file loads LAST: a
 * duplicate declaration in it silently overrode the token layer, and the post-
 * design QA round caught the product still rendering 16px body text while every
 * other measurement said the design system had been adopted. One declaration,
 * in one place, is the whole point of a token layer.
 *
 * 17px rather than 16 is deliberate: Heebo's x-height runs small, and 16px
 * Hebrew on a phone reads as fine print.
 */

/* ------------------------------------------------------------------------
   Layout
   ------------------------------------------------------------------------ */
.container {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 1.25rem;
}

/* ------------------------------------------------------------------------
   The application shell moved.

   `.site-header`, `.brand`, `.site-nav`, `.mobile-tabs` and `.nav-badge` were
   retired here when `partials/header.ejs` was rebuilt on the `rh-header` /
   `rh-tabbar` components: one navigation vocabulary, rendered once and used by
   both the phone and the desktop, so the two can no longer disagree about what
   a destination is called.

   Proved unreferenced before deletion - zero matches across `src/views` and
   `src/public/js` - and the two things worth keeping came with it:

     - the WP-16 `:not(.btn)` fix, which stopped a nav rule repainting the
       signed-out header's call to action at 1.00:1 contrast. It cannot recur:
       `rh-header__link` and `rh-btn` are different classes on different
       elements and never compete for one declaration.
     - the `[CSS3 3/5] text-shadow` proof on the wordmark, which moved to
       `.rh-header__brand` in `components.css` rather than being dropped. The
       design system drops it; the course requires it visible, and course
       demonstrability outranks design fidelity here.
   ------------------------------------------------------------------------ */

/* Height is the shell's job now - `body` is a column and `main` is the part
   that grows. The `min-height: 60vh` that used to live here made the empty
   ground under the footer smaller without removing it. */
main {
  padding: 2.5rem 0 4rem;
}

/*
 * A ROUTE THAT IS STILL WAITING RESERVES THE SCREEN IT WILL FILL. `CP43-01`.
 *
 * Without it the whole document during a route load is a header, one line of
 * text and a footer, so the footer sits mid-viewport and is pushed a full screen
 * down the moment the content arrives. Measured on the recipe page by
 * `QA/product-embed-performance.js`: CLS 1.23 to 1.27, of which 0.919 is a
 * single shift naming `.container` and `.site-footer`.
 *
 * `100vh` rather than an estimate of the content's height, because the property
 * that removes the shift is the footer being BELOW the fold before the content
 * arrives - an element outside the viewport on both sides of a shift contributes
 * nothing. Anything shorter moves the footer less and still moves it.
 *
 * It is on the loading state and not on `main`, so it applies exactly while a
 * screen is waiting and never to a settled page - which is what the note above
 * records going wrong the last time height was put on the shell.
 */
.state-loading--page {
  min-height: 100vh;
}

.site-footer {
  border-top: 1px solid var(--border);
  padding: 1.5rem 0;
  color: var(--muted);
  font-size: 0.85rem;
  background: var(--surface);
}

/* ------------------------------------------------------------------------
   Typography
   ------------------------------------------------------------------------ */
h1,
h2,
h3 {
  line-height: 1.25;
  margin: 0 0 0.6rem;
}

h1 {
  font-size: 2rem;
  /* [CSS3 3/5] text-shadow */
  text-shadow: 0 1px 2px rgba(44, 38, 34, 0.12);
}

h2 {
  font-size: 1.35rem;
}

.lead {
  color: var(--muted);
  font-size: 1.05rem;
  max-width: 60ch;
}

.muted {
  color: var(--muted);
}

.card__title {
  margin: 0 0 0.25rem;
  font-size: 1.05rem;
}

.card__title a {
  color: inherit;
  text-decoration: none;
  transition: color var(--speed) ease;
}

.card__title a:hover {
  color: var(--accent);
}

.grid {
  display: grid;
  gap: 1.1rem;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* ========================================================================
   [CSS3 5/5] multiple-columns
   A short feature list and long ingredient lists flow into balanced columns
   instead of running down one narrow strip.
   ======================================================================== */
/*
 * Mobile-first, and on the ONE breakpoint.
 *
 * This used to be three columns by default, narrowing at 820 and again at 560 -
 * two breakpoints that existed nowhere else in the product, so a phone and a
 * tablet reflowed at widths the rest of the layout knew nothing about. One
 * column by default, three where there is room.
 */
.columns {
  column-count: 1;
  column-gap: var(--space-7);
  column-rule: none;
}

.columns > * {
  /* Keep an item from being split across a column boundary. */
  break-inside: avoid;
  margin-top: 0;
}

@media (min-width: 720px) {
  .columns {
    column-count: 3;
    column-rule: 1px solid var(--color-border);
  }
}

/* ------------------------------------------------------------------------
   Forms
   ------------------------------------------------------------------------ */
/*
 * `.form` no longer sets its own width.
 *
 * It carried `max-width: 460px` with no margin, which meant every form in the
 * product was 460px wide and PINNED to the inline-start edge of whatever
 * contained it. At 1280 the recipe editor rendered 460px inside its own 720px
 * frame with 260px of dead space beside it, and the login card sat against one
 * side of the viewport.
 *
 * Width is owned by the page FRAME now - `.page-editor`, `.page-auth`,
 * `.page-read` - and a form fills the frame it is given. One owner, no
 * specificity stack.
 */


/*
 * `hidden` MUST WIN over any layout display, everywhere.
 *
 * The browser's own rule is `[hidden] { display: none }`, which any author
 * `display` overrides on specificity - so `<form class="row" hidden>` was
 * VISIBLE, because `.row { display: flex }` beat it. On the chat page that
 * meant the composer appeared before a conversation was chosen, and typing
 * into it did nothing at all: the submit handler returns early with no
 * recipient, so the message went nowhere and said nothing.
 *
 * `!important` is the right tool exactly here and almost nowhere else. The
 * attribute is not a style, it is a statement that the element is not part of
 * the page, and no layout class should be able to argue with it.
 *
 * This replaces the `.alert[hidden]` special case, which was the same fix
 * applied to one component after the same collision.
 */
[hidden] {
  display: none !important;
}

/* ------------------------------------------------------------------------
   React media lab. These class names are used only inside #react-media-root.
   ------------------------------------------------------------------------ */
.media-lab__grid {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}

.media-lab__video,
.media-lab__canvas {
  width: 100%;
  display: block;
  background: #1d1a18;
  /* [CSS3 2/5] border-radius */
  border-radius: var(--radius-sm);
  aspect-ratio: 16 / 9;
}

.media-lab__canvas {
  background: #fff;
  border: 1px dashed var(--border);
  cursor: crosshair;
  touch-action: none; /* so a drag draws instead of scrolling the page */
}

/* ------------------------------------------------------------------------
   Chat
   ------------------------------------------------------------------------ */
.chat {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: minmax(220px, 300px) 1fr;
  align-items: start;
}

/*
 * THE CHAT IS A SPLIT, AND A SPLIT FILLS THE ROOM IT IS IN.
 *
 * Two independent reviews found the same thing. The design re-review, against
 * the frontend WP-18 later deleted: "Chat's split exists but its panes float
 * and die ~250px short of the viewport (a void below both cards at 1280)."
 * The spot-check on the current React build, months later: "חלון השיחה לא
 * ממלא את גובה המסך - הרבה לבן ריק מתחת."
 *
 * The same defect survived a full frontend rewrite, which is worth saying out
 * loud: it was never in the markup. It is `align-items: start` on the grid
 * above. That makes every column size to its own CONTENT, so the conversation
 * list came out 176px tall beside a 768px thread and the white below it was
 * simply the page.
 *
 * `start` is kept for the stacked phone layout, where a list that stretched to
 * the viewport would push the thread off the screen entirely. From the width
 * the columns appear at, the panes stretch and each scrolls inside itself -
 * which is what makes a split a split rather than two cards in a row.
 *
 * `100dvh` minus what sits above it, taken from the element rather than
 * guessed: `--fl-chat-top` is set on the page shell so the arithmetic has one
 * home. `dvh` and not `vh` because a mobile browser's chrome moves.
 */
/*
 * F6. THE PHONE SHOWS ONE HALF, AND THE DESKTOP SHOWS BOTH.
 *
 * Below the width the columns appear at, the split is not a split - it is two
 * stacked blocks, and the second one was a 506px panel containing the sentence
 * "בחרו שיחה" under a list the reader was already looking at. `ChatPage` puts
 * `data-open` on the grid; this is the half that acts on it.
 *
 * `display: none` rather than a class swap on the elements: both halves stay
 * mounted, so the thread's `role="log"` live region and the socket's message
 * handling never see a remount, and nothing about the desktop layout below
 * changes.
 */
@media (max-width: 719px) {
  .chat.page-split[data-open="list"] > .chat__conversation { display: none; }
  .chat.page-split[data-open="thread"] > .chat__pane { display: none; }
}

/* The way back is a phone control. On the desktop the list is right there. */
.chat__back { align-self: start; margin-block-end: var(--space-3); }
@media (min-width: 720px) { .chat__back { display: none; } }

@media (min-width: 720px) {
  .chat.page-split {
    align-items: stretch;
    block-size: calc(100dvh - var(--fl-chat-top, 18rem));
    min-block-size: 32rem;
  }

  /* Each pane owns its own scrollbar. Without this the taller one sets the
     height of the grid and the shorter one grows white space to match. */
  .chat.page-split > .chat__pane,
  .chat.page-split > .chat__conversation {
    block-size: 100%;
    min-block-size: 0;
    overflow-y: auto;
  }
}

/*
 * A grid item will not shrink below its longest word unless told to.
 *
 * `min-width` defaults to `auto` on a grid child, which resolves to its
 * min-content width - so ONE unbroken 66-character string in a message pushed
 * the whole column wider than the phone and took the page with it. Measured at
 * 390px: a 375px viewport against a 669px scroll width, with the thread, every
 * bubble, the conversation cards and the composer all overflowing.
 *
 * Chat is where this was always going to surface first: a URL, a file path or
 * somebody holding down a key is ordinary message content, not an edge case.
 */
.chat > * {
  min-width: 0;
}

/* One column below the single breakpoint; the sidebar returns above it. */
@media (max-width: 719px) {
  .chat {
    grid-template-columns: 1fr;
  }
}

.chat__conversation {
  border: 1px solid var(--border);
  background: var(--surface);
  /* [CSS3 2/5] border-radius · [CSS3 4/5] transition */
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  font: inherit;
  color: inherit;
  transition: border-color var(--speed) ease, background-color var(--speed) ease;
}

.chat__conversation:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.chat__thread {
  min-height: 320px;
  max-height: 52vh;
  overflow-y: auto;
  padding: 0.5rem;
  background: var(--bg);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

.chat__bubble {
  max-width: 75%;
  /*
   * Break inside a word when there is no other choice. `anywhere` rather than
   * `break-word` so the bubble's own min-content width also collapses, which is
   * what stops an unbreakable string widening the container before the text
   * ever gets a chance to wrap.
   */
  overflow-wrap: anywhere;

  /*
   * Each message takes its direction from its OWN first strong character.
   *
   * The interface is RTL, so an English message inherited RTL and had its
   * punctuation thrown to the wrong end: "This is a message." rendered as
   * ".This is a message". Chat is the one screen where that matters, because
   * members really do mix Hebrew and English inside a single thread and often
   * inside a single message.
   *
   * `plaintext` is the bidi algorithm's own answer to this - it is what a
   * messaging client should use and what the browser applies to a `<textarea>`
   * by default.
   */
  unicode-bidi: plaintext;
  margin-bottom: 0.6rem;
  padding: 0.55rem 0.85rem;
  background: var(--surface);
  border: 1px solid var(--border);
  /* [CSS3 2/5] border-radius - the flat corner marks who is speaking. */
  border-radius: var(--radius) var(--radius) var(--radius) 4px;
}

/*
 * The message text itself, and this is the element that has to carry it.
 *
 * `unicode-bidi` applies per BOX. Setting it on the bubble fixed the bubble's
 * own children - the timestamp moved to the right end - but the body text lives
 * in its own paragraph, and that paragraph was still inheriting the page's RTL.
 * So an English sentence still rendered as ".This is a message".
 */
.chat__bubble p {
  unicode-bidi: plaintext;
  text-align: start;
}

.chat__bubble--mine {
  margin-inline-start: auto;
  background: var(--accent-soft);
  border-color: #eddcd2;
  border-radius: var(--radius) var(--radius) 4px var(--radius);
}

/* ------------------------------------------------------------------------
   D3 charts
   ------------------------------------------------------------------------ */
.chart {
  width: 100%;
  min-height: 320px;
}

.chart svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

.chart .bar,
.chart .dot {
  fill: var(--accent);
  /* [CSS3 4/5] transition - bars grow into place and lift on hover. */
  transition: fill var(--speed) ease, opacity var(--speed) ease;
}

.chart .bar:hover,
.chart .dot:hover {
  fill: var(--accent-dark);
}

.chart .axis text {
  fill: var(--muted);
  font-size: 0.75rem;
}

.chart .axis path,
.chart .axis line {
  stroke: var(--border);
}

.chart .line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.5;
}

.chart__tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--text);
  color: #fff;
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-sm);
  font-size: 0.8rem;
  opacity: 0;
  transition: opacity var(--speed) ease;
  white-space: nowrap;
  z-index: 5;
}

/*
 * [G-6] The data behind each chart.
 *
 * Deliberately VISIBLE and not screen-reader-only. The value of a bar was
 * readable exactly one way - by hovering it - so a phone had no way to read
 * one at all, and the axis only ever carried the labels that fitted. Hiding
 * this table from sight would answer the accessibility half of the row and
 * leave the touch half where it was.
 *
 * Counts are `tabular-nums` because these columns are read down, not across,
 * and proportional digits make a column of numbers ragged.
 */
.chart-data {
  width: 100%;
  margin-top: 1rem;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.chart-data caption {
  text-align: start;
  padding-bottom: 0.4rem;
  color: var(--muted);
  font-size: 0.8rem;
}

.chart-data th,
.chart-data td {
  padding: 0.35rem 0.5rem;
  text-align: start;
  border-bottom: 1px solid var(--border);
}

.chart-data thead th,
.chart-data tbody th {
  font-weight: 400;
}

.chart-data thead th {
  color: var(--muted);
}

.chart-data td,
.chart-data thead th:last-child {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

.chart-data tfoot th,
.chart-data tfoot td {
  font-weight: 700;
  border-bottom: 0;
}

/*
 * The allergen statement.
 *
 * This is the one block on the page where "renders correctly but looks like
 * everything else" is a real failure. An empty allergen list means either
 * "checked, contains none" or "nobody has checked", the markup already says
 * which, and the styling has to make the second one impossible to skim past -
 * so the unreviewed state gets a border, a tint and a weight the reviewed one
 * does not.
 */
.allergen-notice {
  margin: 0.75rem 0;
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 0.9rem;
  color: var(--muted);
}

.allergen-notice--warn {
  border-color: #e0a33c;
  background: #fdf6e9;
  color: #6d4a12;
  font-weight: var(--weight-bold);
}

/*
 * `.check` is a component now (`components.css`), and only `break-inside` is
 * left here because it belongs to the COLUMN layout this file owns rather than
 * to the control.
 *
 * The rest was deleted because this file loads last: a duplicate declaration
 * silently overrode the component, and the symptom was a 16px hit area on
 * every taxonomy checkbox while the component said 44. The same shape as the
 * body font-size a round earlier - one declaration in one place, or the token
 * layer is decoration.
 */
.check {
  break-inside: avoid;
}

/*
 * The picture gallery below the steps. Auto-fill rather than a fixed count, so
 * one extra photo does not need a new rule and a phone gets one column without
 * a media query.
 */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 0.75rem;
}

.gallery img,
.gallery video,
.gallery iframe {
  width: 100%;
  height: 160px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 0;
}

/*
 * The imported source embed. WP-12.
 *
 * A 9:16 aspect box rather than a fixed height, because the three platforms we
 * embed are all vertical video and a landscape frame letterboxes every one of
 * them. `max-width` keeps it from becoming a wall on a desktop, and `aspect-
 * ratio` means a phone needs no media query - the frame is always as tall as
 * its own width demands.
 */
/*
 * THE SOURCE PLAYER. `PRODUCT-EMBED-01`, CP-38.
 *
 * THE BOX IS RESERVED ON THE WRAPPER, not on the frame, because the poster, the
 * button and the frame have to occupy the SAME space - that is what makes
 * pressing play move nothing on the page.
 *
 * `--fl-embed-aspect` is set inline from `source.embedAspect`, which the server
 * takes from the adapter for that provider. The `9 / 16` here is the fallback
 * for a value the server did not send, and it used to be the ONLY value: one
 * hard-coded portrait ratio for every provider, so a YouTube video - 16:9 by
 * definition - was framed in a portrait box on every recipe imported from it.
 */
.source-embed {
  position: relative;   /* the containing block for CP-41's two failure layers */
  max-width: 340px;
  margin-inline: auto;
  aspect-ratio: var(--fl-embed-aspect, 9 / 16);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  overflow: hidden;
}

.source-embed iframe,
.source-embed__play {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* The poster and its cue. One control, and it is the only one this product
   draws over a player - once the frame is there it unmounts, because a control
   that does not drive the player it sits on is forbidden outright. */
.source-embed__play {
  position: relative;
  padding: 0;
  background: var(--bg);
  color: inherit;
  cursor: pointer;
}

.source-embed__play img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.source-embed__cue {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem 0.5rem;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 0.95rem;
  text-align: center;
}

.source-embed__play:hover .source-embed__cue,
.source-embed__play:focus-visible .source-embed__cue {
  background: rgba(0, 0, 0, 0.72);
}

/*
 * THE FOCUS RING, DRAWN INSIDE THE BOX. `CP39-01`.
 *
 * `tokens.css` gives the whole product one ring at `outline-offset: 2px`, and
 * for this one control that offset put it entirely OUTSIDE a wrapper which is
 * `overflow: hidden` - the button fills `.source-embed` exactly, so all four
 * sides were clipped away. `QA/product-embed-player.js` measured it at 320,
 * 390, 768 and 1280 for all three framable providers: focus changed nothing but
 * the caption's tint, which is not a focus indicator.
 *
 * A NEGATIVE offset rather than `overflow: visible` on the wrapper: the wrapper
 * clips for a reason - a provider's frame is a document this product does not
 * control, and it stays inside the rounded box it was given.
 */
.source-embed__play:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: -4px;
}

/*
 * THE TWO FAILURE STATES. `PRODUCT-EMBED-01` clause 3, CP-41.
 *
 * BOTH FILL THE SAME RESERVED BOX. The wrapper's `aspect-ratio` is on
 * `.source-embed` and applies in all four states, so a player that never
 * arrives leaves the page exactly where it was - which is the same requirement
 * as the one the poster satisfies, asked of the case where nothing works.
 *
 * The still is drawn UNDER the explanation rather than replaced by it, because
 * Product's clause is "the cover, a short explanation, and a control to the
 * source" and the cover is the first of the three.
 */
.source-embed__backdrop,
.source-embed__failed {
  position: absolute;
  inset: 0;
  display: block;
}

.source-embed__backdrop img,
.source-embed__failed > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
 * The backdrop is BEHIND the frame, never over it. A player is a document this
 * product does not control and does not draw on: `z-index: 0` with the frame at
 * `1` is the whole of that rule, and the frame unmounts the backdrop anyway the
 * moment it commits.
 */
.source-embed__backdrop { z-index: 0; }
.source-embed iframe { position: relative; z-index: 1; }

/*
 * The panel dims the still rather than hiding it, so the recipe's own picture
 * stays legible under a sentence that has to be readable over it.
 */
.source-embed__failed-panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3, 0.75rem);
  padding: var(--space-4, 1rem);
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  text-align: center;
}

.source-embed__failed-panel p {
  margin: 0;
  font-size: 0.9rem;
  line-height: 1.45;
}

/* The retry sits on a dark panel, so it takes its colours from it rather than
   from the page's own text token, which is near-black on this background. */
.source-embed__failed-panel .rh-btn {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.55);
  color: #fff;
}

.source-embed__failed-panel .rh-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.26);
  border-color: #fff;
  color: #fff;
}

/*
 * THE SENTENCE UNDER THE PLAYER RESERVES BOTH OF ITS STATES. `CP39-02`.
 *
 * One cell, two lines, the inactive one hidden but still occupying its box, so
 * the paragraph is as tall as the taller sentence in both states. Pressing play
 * used to shorten it by a line at 320 and 390 and move the rest of the page up
 * 23px.
 */
.source-embed__note {
  display: grid;
}

.source-embed__note-line {
  grid-area: 1 / 1;
}

.source-embed__note-line[data-live='false'] {
  visibility: hidden;
}

/* What an import found, and what it did not. Facts, one per line. */
.import-found,
.import-demo {
  margin: 0.5rem 0 0;
  padding-inline-start: 1.1rem;
  font-size: 0.9rem;
  color: var(--muted);
}

.import-demo {
  list-style: none;
  padding-inline-start: 0;
}

.import-demo li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.35rem;
}

/*
 * A URL has no spaces, so on a phone it is the one thing on this page that can
 * push the layout wider than the viewport. It breaks anywhere rather than
 * scrolling the whole document sideways.
 */
.import-demo code {
  overflow-wrap: anywhere;
  font-size: 0.75rem;
}

/* The source row in the editor: one line, elided, never a wrapped wall. */
.media-source-url {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  direction: ltr;
  min-width: 0;
}

/* The demo links sit inside a sentence, so their buttons are sentence-sized. */
/*
 * R4-1. `.btn--small` IS GONE, AND IT SHOULD NEVER HAVE BEEN A SECOND THING.
 *
 * Two modifiers meant the same thing and only one of them was safe.
 * `components.css` defines `.btn--sm` and it carries `min-height:
 * var(--tap-min)`; this one carried `padding: 0.2rem` and no floor at all, so
 * every control wearing it measured about 26px.
 *
 * Eight of them, and they were not obscure: five AI actions in the editor -
 * fill, confirm, edit, save, cancel - plus one in the import panel. The whole
 * of the AI assist was under the touch target, on the screen where a member is
 * accepting or rejecting what a model wrote for them.
 *
 * The rule is deleted rather than given a floor. A second name for one idea is
 * how the unsafe one stays reachable: `btn--sm` already existed, already
 * complied, and was already what the rest of the product used.
 */

/*
 * AI Assist. WP-13.
 *
 * The panel deliberately reuses `.card`, `.toolbar`, `.field` and `.badge`
 * rather than inventing a look of its own - an AI feature that arrives in a
 * different visual language reads as bolted on, and this one is an ordinary
 * part of writing a recipe.
 */
.ai-panel__head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.ai-panel__head h2 {
  margin: 0;
}

.ai-review__summary {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

/*
 * Warnings are the useful half of an AI answer, so they are given the weight of
 * the allergen notice rather than the weight of a caption: a member who skims
 * past "sugar appears in the steps but not in the ingredients" has been given
 * the information and has still been let down.
 */
.ai-warnings {
  margin: 0.5rem 0 0.75rem;
  padding: 0.6rem 0.85rem;
  padding-inline-start: 1.9rem;
  border: 1px solid #e0a33c;
  border-radius: var(--radius-sm);
  background: #fdf6e9;
  color: #6d4a12;
  font-size: 0.9rem;
}

.ai-warnings li {
  margin-bottom: 0.3rem;
}

.ai-warnings li:last-child {
  margin-bottom: 0;
}

/* A textarea is the one control here that a phone needs room for. */
.ai-panel textarea {
  min-height: 7rem;
  resize: vertical;
}

/*
 * The status line takes a row of its own once the buttons have wrapped.
 *
 * It is a flex item next to two buttons, and its longest message is a full
 * Hebrew failure sentence. Left to shrink, at 390px it became a two-word-wide
 * column beside the buttons - readable in principle and unreadable in
 * practice. `flex-basis: 100%` after the first wrap gives it the whole line.
 */
.ai-panel .toolbar #ai-status {
  flex: 1 1 14rem;
  min-width: 0;
  align-self: center;
}

/*
 * The proposal's own text can be long - an ingredient name, a step, a warning
 * naming five ingredients - and none of it may push the page sideways.
 */
.ai-panel,
.ai-panel .ai-warnings li,
.ai-panel #ai-found,
.ai-panel #ai-status {
  overflow-wrap: anywhere;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: end;
  margin-bottom: 1.5rem;
}

.toolbar .field {
  margin-bottom: 0;
  min-width: 180px;
}

.stack > * + * {
  margin-top: 1.25rem;
}

.row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  /*
   * Wrapping is the default, and the visual gate is why.
   *
   * A non-wrapping flex row of eight buttons is wider than a 390px phone, and
   * a flex child does not shrink below its content - so the row did not
   * overflow visibly, it made the whole PAGE wider than the viewport and pushed
   * every heading off the edge. The recipe page was unreadable on a phone and
   * no test noticed, because the markup was correct.
   */
  flex-wrap: wrap;
}

.empty {
  display: grid;
  justify-items: center;
  gap: var(--space-4);
  text-align: center;
  padding: 3rem 1rem;
  color: var(--muted);
  text-wrap: pretty;
}

/* FIND-P2-06. The sentence and the way out of it - one action, never three. */
.empty__text { margin: 0; max-inline-size: 42ch; }

/* ==========================================================================
   C-5 — THE PERSONAL LIBRARY SEGMENTS.
   ==========================================================================

   Saved, Created and Imported were three screens that never referred to each
   other. They are one world now and this is the only new furniture: three
   links, one of them current. Links rather than tabs, because each is a real
   address and `role="tab"` would promise a tabpanel these pages do not have.
   ========================================================================== */

.libnav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-block-end: var(--space-5);
}

.libnav__link {
  min-block-size: var(--tap-min);
  min-block-size: var(--tap-min);
  display: inline-flex;
  align-items: center;
  padding-inline: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font: var(--type-label);
  text-decoration: none;
}

.libnav__link:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

/* Painted from the attribute that states it, so the look and the announcement
   cannot disagree about which segment is open. */
.libnav__link[aria-current="page"] {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

.is-loading {
  opacity: 0.55;
  transition: opacity var(--speed) ease;
}

/* Visible only to screen readers, used for live status announcements. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 1ms !important;
  }
}

/*
 * Not-interested collapses the card in place and leaves the undo.
 *
 * The card is not removed: the control is one mis-tap away on a scrolling feed,
 * and a hide with no way back teaches people not to touch it. The next feed
 * request is what actually drops it.
 */
.card__hidden-note {
  display: none;
  margin: 0;
  color: var(--muted);
  font-size: 0.9rem;
}

.card.is-hidden > *:not(.card__hidden-note) {
  display: none;
}

.card.is-hidden .card__hidden-note {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}


/* ==========================================================================
   The vertical feed. Mobile first, RTL by inheritance.
   ==========================================================================

   Written narrow-first and widened with one min-width query, rather than the
   other way round: the feed IS the phone experience, and a desktop layout
   squeezed down always ends up with something that only makes sense wide.

   Nothing here uses `left` or `right`. The document is `dir="rtl"`, so the
   logical properties (`inline-start`, `margin-inline`) put things on the
   correct side without a mirrored stylesheet - and they stay correct if a
   single page is ever rendered LTR.
*/

.feed {
  max-width: 620px;
  margin-inline: auto;
}

.feed__header {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--bg);
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--border);
}

.feed__title {
  font-size: 1.25rem;
  margin: 0 0 0.5rem;
}

.feed__tabs {
  display: flex;
  gap: 0.5rem;
}

.feed__tab {
  flex: 1;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-weight: var(--weight-bold);
  cursor: pointer;
}

/* State in aria-selected, so the styling and the screen reader cannot drift. */
.feed__tab[aria-selected='true'] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.feed__notice {
  margin: 0.75rem 0 0;
  padding: 0.5rem 0.75rem;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent-dark);
  font-size: 0.9rem;
}

.feed__list {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  padding-block: 1.25rem;
}

.feed__empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--muted);
}

.feed__more {
  text-align: center;
  padding-block: 1rem;
}

/* -- one card -------------------------------------------------------------

   MEDIA FIRST, and the reason the card was rebuilt.

   The first version was a picture inside a card: a media box, then a title,
   then a meta line, then a row of outlined pills. Everything was present and it
   read as a CRUD record with a photograph attached, because the layout said the
   text was the content and the picture was an illustration of it.

   Here the media IS the card. The creator, the title and the actions are layered
   ON it, the way a social feed does it, and the only thing below the frame is
   the explanation - which belongs under the food rather than over it.
*/

.feed-card {
  background: transparent;
  border: 0;
  border-radius: var(--radius);
}

.feed-card__frame {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg);
  box-shadow: 0 2px 6px rgba(44, 38, 34, 0.10), 0 18px 40px rgba(44, 38, 34, 0.12);
}

.feed-card__link {
  display: block;
}

/*
 * A tall, FIXED aspect ratio.
 *
 * 4:5 because that is the shape a phone held upright wants and the shape the
 * generated covers are drawn at. Fixed because letting each image set its own
 * height makes the feed jump as pictures load - the reader taps what was under
 * their thumb a moment ago.
 */
.feed-card__media {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  background: var(--accent-soft);
}

.feed-card__media--none {
  background: linear-gradient(140deg, var(--accent-soft), #e7d7c6);
}

/* -- the layer over the media --------------------------------------------- */

/*
 * A scrim, not a solid bar.
 *
 * White text needs a dark ground to stay readable over an unknown picture, and
 * a solid bar would cover the food. The gradient is transparent where the image
 * matters and opaque where the words are.
 */
.feed-card__scrim {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: 3.5rem 0.9rem 0.9rem;
  background: linear-gradient(to top,
    rgba(20, 16, 13, 0.88) 0%,
    rgba(20, 16, 13, 0.62) 45%,
    rgba(20, 16, 13, 0) 100%);
  color: #fff;
  /* The rail sits beside this; the scrim must not swallow its taps. */
  padding-inline-end: 4.5rem;
}

.feed-card__creator {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.feed-card__creator-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}

.feed-card__avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  border: 2px solid rgba(255, 255, 255, 0.85);
}

.feed-card__avatar--initial {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: #fff;
  font-weight: 700;
  font-size: 1rem;
}

.feed-card__creator-name {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  font-weight: 700;
  font-size: 0.92rem;
  min-width: 0;
}

.feed-card__handle {
  font-weight: 400;
  font-size: 0.78rem;
  opacity: 0.75;
  direction: ltr;
  text-align: start;
}

/* Small, outlined, and on the card - a creator you can follow without leaving. */
.feed-card__follow {
  margin-inline-start: auto;
  flex: none;
  padding: 0.25rem 0.85rem;
  border-radius: var(--radius-pill);
  border: 1.5px solid rgba(255, 255, 255, 0.9);
  background: transparent;
  color: #fff;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
  transition: background var(--speed) ease, color var(--speed) ease;
}

.feed-card__follow:hover,
.feed-card__follow[aria-pressed='true'] {
  background: #fff;
  color: var(--text);
}

.feed-card__title {
  margin: 0 0 0.15rem;
  font-size: 1.2rem;
  line-height: 1.25;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}

.feed-card__title a {
  color: inherit;
  text-decoration: none;
}

.feed-card__meta {
  margin: 0;
  font-size: 0.85rem;
  opacity: 0.9;
}

/* -- the badges, top of the frame ----------------------------------------- */

.feed-card__top {
  position: absolute;
  inset-inline-start: 0.75rem;
  top: 0.75rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  max-width: 70%;
}

/*
 * The match badge: the product's signature, at the top of the card.
 *
 * It was a collapsed `<details>` at the bottom. Explainability that has to be
 * opened is explainability nobody sees, and this number is the one thing on the
 * card that says the feed was assembled FOR this person.
 */
.feed-card__match {
  background: rgba(255, 255, 255, 0.94);
  color: var(--accent-dark);
  font-weight: 700;
  font-size: 0.8rem;
  padding: 0.25rem 0.7rem;
  border-radius: var(--radius-pill);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}

.feed-card__warn {
  background: rgba(253, 246, 233, 0.96);
  color: #6d4a12;
  border: 1px solid #e0a33c;
  font-weight: 700;
  font-size: 0.75rem;
  padding: 0.2rem 0.6rem;
  border-radius: var(--radius-pill);
}

/* -- why recommended, under the frame ------------------------------------- */

.feed-why {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.feed-why__item {
  font-size: 0.78rem;
  color: var(--accent-dark);
  background: var(--accent-soft);
  border-radius: var(--radius-pill);
  padding: 0.2rem 0.65rem;
}

/* A penalty is a reason too, and must not look like a recommendation. */
.feed-why__item[data-signal='dislikedIngredient'] {
  color: var(--danger);
  background: #fbeaea;
}

.feed-card__hidden-note {
  display: none;
}

.feed-card.is-hidden .feed-card__frame,
.feed-card.is-hidden .feed-why {
  display: none;
}

.feed-card.is-hidden .feed-card__hidden-note {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 1.25rem;
  margin: 0;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

@media (min-width: 720px) {
  /* Slightly less tall with a mouse, where a full 4:5 needs scrolling to pass. */
  .feed-card__media {
    aspect-ratio: 1 / 1;
  }
}


/* ==========================================================================
   Mobile navigation
   ==========================================================================

   The visual gate found this, and no test could have: on a 390px viewport the
   thirteen-link top nav wrapped into a block tall enough to fill the entire
   screen, so every page rendered its content below the fold. The DOM was
   correct - ten feed cards were present and the tests that count them passed -
   and a person holding a phone saw nothing but navigation.

   The fix is not to shrink the list. A product whose primary surface is a feed
   needs its primary destinations under the thumb, so on a phone the top bar
   keeps the wordmark and the sign-out only, and the five `data-primary`
   destinations move to a bottom tab bar. The rest of the links stay in the top
   nav on wider screens, where there is room for them.
*/

@media (max-width: 719px) {


  /*
   * The secondary links are hidden on a phone rather than collapsed behind a
   * hamburger. A menu that has to be opened to discover what is in it is worse
   * than five permanent tabs plus a profile page that lists the rest, and it is
   * one more piece of state to get wrong.
   */


  main {
    /* Room for the bar, plus the home indicator on a notched phone. */
    padding: 1rem 0 calc(4.5rem + env(safe-area-inset-bottom, 0px));
  }

  

  /* The feed's own sticky header sits under the site header, not over it. */
  .feed__header {
    top: 52px;
  }
}


/* ==========================================================================
   Explore: shelves
   ==========================================================================

   Horizontal rails rather than a grid per collection. Ten collections in a
   vertical grid is ten screens of scrolling on a phone; a rail says "a slice of
   something bigger" and costs one screen each.
*/

.explore {
  max-width: 1080px;
  margin-inline: auto;
}

.explore__header {
  margin-bottom: 1.25rem;
}

.explore__title {
  font-size: 1.4rem;
  margin: 0 0 0.75rem;
}

.explore__search {
  display: flex;
  gap: 0.5rem;
}

.explore__search input {
  flex: 1;
  min-width: 0;
  padding: 0.7rem 0.9rem;
  font: inherit;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
}

.explore__search input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.shelf {
  margin-bottom: 2rem;
}

.shelf__header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.6rem;
}

.shelf__title {
  font-size: 1.1rem;
  margin: 0;
}

.shelf__subtitle {
  margin: 0;
  font-size: 0.82rem;
  color: var(--muted);
}

.shelf__more {
  flex: none;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent);
  text-decoration: none;
}

/*
 * The rail. Scroll snapping so a swipe lands on a card rather than between two,
 * and the scrollbar hidden because on a phone it is a smear over the art.
 */
.shelf__rail {
  display: flex;
  gap: 0.75rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: 0.35rem;
  scrollbar-width: none;
}

.shelf__rail::-webkit-scrollbar {
  display: none;
}

.shelf__item {
  flex: none;
  width: 46vw;
  max-width: 190px;
  scroll-snap-align: start;
}

.card__media {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--accent-soft);
}

.card__media--none {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  background: linear-gradient(140deg, var(--accent-soft), #e7d7c6);
}

.card__meta {
  margin: 0 0.6rem;
  font-size: 0.78rem;
  color: var(--muted);
}

.card__warn {
  margin: 0.35rem 0.6rem 0;
  font-size: 0.7rem;
  font-weight: 700;
  color: #6d4a12;
  background: #fdf6e9;
  border: 1px solid #e0a33c;
  border-radius: var(--radius-sm);
  padding: 0.15rem 0.4rem;
}

@media (min-width: 720px) {
  .shelf__item {
    width: 200px;
    max-width: none;
  }
}


/* ==========================================================================
   Search, in the discovery language
   ==========================================================================

   The filters did not change - all nine are still there, with the same names -
   but a technical form is the wrong shape on a phone. The quick controls sit in
   a bar and the rest live in a sheet, because the RESULT is what somebody came
   for and the filters are an adjustment to it.
*/

.search-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.75rem;
}

.search-head__title {
  font-size: 1.4rem;
  margin: 0;
}

.search-head__back {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent);
  text-decoration: none;
}

/* Pills, matching the feed's tabs, so the two surfaces read as one product. */
.search-tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.search-tabs__tab {
  flex: 1;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-weight: 700;
  font-size: 0.9rem;
  cursor: pointer;
}

.search-tabs__tab[aria-selected='true'] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.search-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* The row breaks before either control does. See `.search-bar__filters`. */
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.search-bar__filters {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font: inherit;
  font-weight: 700;
  font-size: 0.9rem;
  cursor: pointer;
  /*
   * The label holds one line, and the pill does not shrink to force it off one.
   *
   * `QA/content-fit.js` found this by stressing the label: at 320 the toggle
   * shared a `space-between` row with the sort, was squeezed to its minimum,
   * and laid `ייבוא מ-Instagram` out on THREE rows in an 83px-tall pill, with
   * `מ-Instagram` split between its letters. The real Hebrew label happens to
   * fit, which is exactly the kind of luck the stress pass exists to remove.
   *
   * `flex-shrink: 0` here and `flex-wrap` on the bar are one decision: when
   * there is not room for both controls side by side, the row breaks rather
   * than the words.
   */
  white-space: nowrap;
  flex-shrink: 0;
}

.search-bar__filters[aria-expanded='true'] {
  border-color: var(--accent);
  color: var(--accent-dark);
}

/* The number of active filters, so "why so few results" answers itself. */
.search-bar__count {
  min-width: 1.35rem;
  height: 1.35rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  font-size: 0.75rem;
}

.search-bar p {
  margin: 0;
  font-size: 0.85rem;
}

/* ==========================================================================
   C-4 — THE SIMPLE SEARCH, AND THE DOOR TO THE REST.
   ==========================================================================

   FIND-P2-04: course-required power at consumer weight. Nine filters across two
   forms opened this page, so somebody who wanted to type "שקשוקה" met a control
   panel first.

   The previous answer was a sheet below 720px, which fixed half of it - the
   complaint was about weight, and the desktop is where the nine-filter wall was
   widest. `#search-advanced-panel` is `hidden` at EVERY width now, so the
   browser's own `[hidden]` does the hiding and this block is only about what
   the two visible things look like.

   NOTHING WAS REMOVED. §12: course compliance is not traded for visual
   simplicity. Every parameter, both forms, the sort, the pagination, the reset
   and the Ajax are behind the door, one press away, at any width.
   ========================================================================== */

.search-simple {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  margin-block-end: var(--space-4);
}

/* The box takes the row; the button takes what it needs. A search field sharing
   its width equally with its own button reads as two controls of equal
   importance, and they are not. */
.search-simple .field {
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
}

.search-simple .btn { flex: 0 0 auto; }

/* No box of its own: the region is the tabs and two forms, which already carry
   `.card`. A frame around a frame reads as a modal. */
.search-panel { margin-block-end: var(--space-4); }

/* The sort sits with the filter door rather than under the forms, because it is
   a RESULT control and the forms are now usually shut. */
.search-bar__sort {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

.search-bar__sort label {
  margin: 0;
  white-space: nowrap;
  color: var(--color-text-muted);
  font: var(--type-caption);
}

@media (max-width: 719px) {
  /*
   * The forms stack on a phone. They are no longer HIDDEN here - the region
   * above them is, at every width - and what is left is the rule that stops
   * four labelled inputs from trying to share 320px as a flex row.
   *
   * SCOPED TO `.toolbar`, because `[data-panel]` is not a component; it is a
   * bare attribute, and a second view had started using it. Unscoped, the
   * display rules that used to live here hid all eight `<section data-panel>`
   * panels of the ADMINISTRATION area on every phone: `public/js/admin.js`
   * switches its tabs with the `hidden` attribute alone, so the admin panels
   * matched the hiding rule and could never match the revealing one. Measured
   * on /admin at the time: every panel `display: none`, height 0, from 320 to
   * 719, with 2935 characters of content inside it. The tab strip kept working,
   * so it moved its underline and revealed nothing.
   *
   * That pair of rules is GONE rather than fixed. `hidden` on the region does
   * the job now, and there is no second, class-based way to hide a panel left
   * for a stylesheet to get wrong.
   */
  [data-panel].toolbar {
    flex-direction: column;
    align-items: stretch;
    gap: 0.25rem;
  }

  [data-panel] .field {
    margin-bottom: 0.6rem;
  }

  /* The button under the box rather than beside it: at 320 a search field and
     its button on one row leaves the field too narrow for a Hebrew word. */
  .search-simple {
    flex-direction: column;
    align-items: stretch;
  }
}


/* ==========================================================================
   Recipe detail, in the discovery language
   ==========================================================================

   The gap this closes: a reader arriving from a feed card where the creator had
   an avatar, a handle and a follow button used to find them demoted to a grey
   "by <blue link>" line. It is the same person and the same relationship, and
   the transition between two screens is what tells somebody whether they are
   in one product or two.

   Nothing structural changed - same markup order, same rail contract. This is
   typography, spacing and the creator block.
*/

.recipe {
  max-width: 720px;
  margin-inline: auto;
}

.recipe__creator {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.75rem;
}

.recipe__creator-link {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}

.recipe__avatar {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
}

.recipe__avatar--initial {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: #fff;
  font-weight: 700;
  font-size: 1.15rem;
}

.recipe__creator-name {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  font-weight: 700;
  font-size: 0.98rem;
}

.recipe__handle {
  font-weight: 400;
  font-size: 0.8rem;
  color: var(--muted);
  direction: ltr;
  text-align: start;
}

.recipe__follow {
  margin-inline-start: auto;
  flex: none;
  padding: 0.3rem 1rem;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--accent);
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  transition: background var(--speed) ease, color var(--speed) ease;
}

.recipe__follow:hover,
.recipe__follow[aria-pressed='true'] {
  background: var(--accent);
  color: #fff;
}

/* Matches the feed card's title scale rather than the old page-heading scale. */
.recipe__title {
  font-size: 1.6rem;
  margin: 0 0 0.3rem;
  text-shadow: none;
}

.recipe__meta {
  margin: 0 0 0.5rem;
  font-size: 0.9rem;
}

/*
 * The cover, framed like a feed card rather than dropped in as a full-bleed
 * banner - so the two screens share a shape as well as a palette.
 */
.recipe > img,
.recipe .gallery img {
  border-radius: var(--radius);
}

.recipe > img {
  box-shadow: 0 2px 6px rgba(44, 38, 34, 0.10), 0 18px 40px rgba(44, 38, 34, 0.10);
}

@media (max-width: 719px) {
  .recipe__title {
    font-size: 1.35rem;
  }

  .recipe .action-rail {
    gap: 0.35rem;
  }

  /*
   * Nothing on this page may be wider than the phone.
   *
   * A belt-and-braces guard rather than a fix: the wrapping above solves the
   * row, and this catches the next element that tries the same thing - a long
   * unbroken title, a wide table, an embed.
   */
  .recipe {
    max-width: 100%;
    overflow-x: hidden;
  }
}

/* ------------------------------------------------------------------------
   The grocery cost panel. WP-13.5A.

   The visual job here is to keep three states legible as DIFFERENT states.
   A complete estimate and a partial one carrying the same weight on the page
   would undo the whole point of separating them, so partial gets a warning
   tint and insufficient gets no number to look at at all.

   Directional properties are the logical forms throughout, so the panel reads
   right-to-left with the rest of the interface rather than fighting it.
   ------------------------------------------------------------------------ */
.pricing {
  margin-top: 1.5rem;
}

.pricing__heading {
  margin: 0 0 0.75rem;
  font-size: 1.05rem;
}

.pricing__total {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
}

.pricing__amount {
  font-size: 1.5rem;
  font-weight: 700;
}

/*
 * The range is not decoration and is not fine print.
 *
 * It is the honest part of the figure - the same 100g of parmesan really does
 * cost between 13 and 16 shekels depending on which one you reach for - so it
 * sits on the same line as the headline number rather than being demoted.
 */
.pricing__range {
  font-size: 0.9rem;
  color: var(--muted);
}

.pricing__serving {
  margin: 0.25rem 0 0;
  color: var(--muted);
}

/* ---- Stage F: three concepts, three visual weights ----------------------
 *
 * The used cost, the shopping cost and the package price are three different
 * claims, and a member who cannot tell them apart at a glance has been told
 * nothing. So they are NOT three identically-styled numbers with different
 * captions - that is the arrangement Stage E found in the review panel, where
 * the words were right and everything else said they were the same thing.
 *
 * The used cost keeps the headline weight because it is what the panel is
 * about. The shopping cost is a second, smaller figure with its own line, and
 * it always carries its caption: an unlabelled number here would be read as
 * the headline one.
 */
.pricing__shopping {
  margin: 0.5rem 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
}

.pricing__shopping .pricing__amount {
  font-size: 1.1rem;
}

/* The caption never leaves the number. It is what makes the number a claim
 * about something rather than "the price". */
.pricing__concept {
  font-size: 0.85rem;
}

.pricing__confidence,
.pricing__coverage {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
}

.pricing__none {
  margin: 0;
  color: var(--muted);
}

.pricing__warning {
  margin: 0.75rem 0 0;
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius);
  background: #fdf2e6;
  color: #8a4b00;
  font-size: 0.9rem;
}

.pricing__details {
  margin-top: 1rem;
  border-top: 1px solid var(--border);
  padding-top: 0.75rem;
}

.pricing__details > summary {
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--muted);
}

.pricing__list {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
}

.pricing__row {
  display: grid;
  grid-template-columns: minmax(6rem, 1fr) auto;
  gap: 0.15rem 0.75rem;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}

.pricing__row:last-child {
  border-bottom: 0;
}

/*
 * An unpriced ingredient is dimmed, never hidden.
 *
 * It stays in the list because it stays in the coverage denominator: the whole
 * claim of this panel is "nine of eleven, and here are the other two". Hiding
 * them would turn an honest partial estimate into a complete-looking one.
 */
.pricing__row--unpriced .pricing__name {
  color: var(--muted);
}

.pricing__value {
  text-align: start;
  white-space: nowrap;
}

/*
 * THE UNKNOWN VALUE IS A STATE, NOT A MISSING NUMBER.
 *
 * It reads `אין אומדן` in words with the reason beneath it, so it needs the
 * weight of an answer rather than the size of a price. Muted, not alarmed: a
 * conversion Flayvo cannot make responsibly is an honest outcome, not an error.
 */
.pricing__value--unknown {
  font: var(--type-body-sm);
  /* 700, not 600: this repository ships two Heebo weights and a third would be
     synthesised by the browser. The design-integrity gate caught it. */
  font-weight: 700;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.pricing__basis {
  grid-column: 1 / -1;
  font-size: 0.8rem;
}

.pricing__footnote {
  margin: 0.9rem 0 0;
  font-size: 0.8rem;
  line-height: 1.5;
}

@media (max-width: 719px) {
  .pricing__amount {
    font-size: 1.25rem;
  }

  /*
   * On a phone the range moves under the figure instead of beside it. Two
   * numbers competing for one narrow line is how "37.88" and "32.81 .. 71.80"
   * become one unreadable string.
   */
  .pricing__total,
  .pricing__shopping {
    display: block;
  }

  .pricing__range,
  .pricing__concept {
    display: block;
    margin-top: 0.15rem;
  }
}


/* ------------------------------------------------------------------------
   The admin area. WP-15.

   Administrative, but still RecipeHub - the same surfaces, radii and spacing
   as the rest of the product. Deliberately NOT dense tables: a table with
   eight columns is unreadable on a phone and has to be rebuilt as cards
   anyway, so the row IS a card at every width and simply gains a second
   column when there is room.
   ------------------------------------------------------------------------ */
.admin-kpis {
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}

.admin-kpi__value {
  font-size: 1.8rem;
  margin: 0;
}

/*
 * The operations strip, quieter than the headline cards on purpose. These are
 * facts an operator checks when something looks wrong, not numbers anybody
 * watches.
 */
.admin-ops {
  margin-top: 1rem;
  padding: 1rem 1.25rem;
}

.admin-ops__title {
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
}

.admin-ops__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.75rem 1.25rem;
  margin: 0;
}

.admin-ops__list dt {
  font-size: 0.8rem;
  color: var(--muted);
}

.admin-ops__list dd {
  margin: 0.15rem 0 0;
  font-weight: 700;
  /* A chain name and a date; it must break rather than widen the grid. */
  overflow-wrap: anywhere;
}

/*
 * One administrable row.
 *
 * `min-width: 0` on the text column for the same reason the chat thread needs
 * it: a long unbroken recipe title or slug would otherwise refuse to shrink
 * and push the whole page sideways.
 */
.admin-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  margin-bottom: 0.6rem;
}

.admin-row__main {
  min-width: 0;
  flex: 1 1 16rem;
  overflow-wrap: anywhere;
}

.admin-row__meta {
  margin: 0.2rem 0 0;
  font-size: 0.85rem;
}

.admin-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* A deprecated term is dimmed and labelled, never hidden - it still exists. */
.admin-row--muted {
  opacity: 0.72;
}

/* Configuration. One label, one control, one hint. */
.config-group {
  margin-bottom: 1rem;
}

/*
 * Ranking is marked, because changing a feed weight changes what every member
 * sees. The tint is the same warning colour the partial-pricing notice uses,
 * so "be careful" looks the same everywhere in the product.
 */
.config-group--advanced {
  border-color: #e8c9a0;
  background: #fdfaf6;
}

.config-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}

.config-row:last-child {
  border-bottom: 0;
}

.config-row__label {
  min-width: 0;
  overflow-wrap: anywhere;
}

/*
 * Numbers are LTR even inside an RTL page, and here that had to be said.
 *
 * A range written `1-100` rendered as `100-1`, and a stored `-30` rendered as
 * `30-` with the minus thrown to the wrong end. Both are the bidi algorithm
 * working correctly on a paragraph it was told is right-to-left; a numeric
 * range simply is not. `isolate` rather than a bare `direction`, so the run
 * cannot affect the label beside it either.
 */
.config-row__hint {
  font-size: 0.75rem;
  white-space: nowrap;
  direction: ltr;
  unicode-bidi: isolate;
}

.config-row input[type="number"] {
  width: 6.5rem;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  direction: ltr;
  text-align: end;
}

@media (max-width: 719px) {
  /*
   * On a phone the actions drop under the text rather than competing with it
   * for a narrow line, and the configuration hint moves under its control.
   */
  .admin-row__actions {
    width: 100%;
  }

  .config-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .config-row__hint {
    grid-column: 1 / -1;
  }
}

/* ------------------------------------------------------------------------
   The action rail moved to `components.css` (`rh-rail`, `rh-rail__btn`).

   The legacy `.action-rail`, `.rail`, `.rail__btn` and `.rail__icon` rules were
   retired here rather than left in place, because this file loads LAST: a
   legacy rule that still matches wins the cascade over the component that
   replaced it, and the symptom is a half-migrated control that looks broken in
   a way neither stylesheet explains on its own.

   The one property worth naming: the state that a screen reader announces is
   the state that is styled. The replacement hangs off the same ARIA attribute
   the retired rule did, so the two cannot drift apart. The selectors are
   deliberately not quoted here - a comment that names a selector can satisfy a
   test that greps for it, which is how a deleted rule keeps passing its own
   regression test.
   ------------------------------------------------------------------------ */

/* ------------------------------------------------------------------------
   Retired to `components.css`: the button ranks, the card, the alert, the
   badge, the avatar and the form field.

   They were declared in BOTH files, and this one loads last, so the legacy
   declaration won every time. That is not a style question - it is why the
   product rendered 16px body text, 16px checkbox hit areas and a "save draft"
   button identical to "publish" while the component layer said otherwise, and
   why three separate rounds each found one instance of it without seeing the
   pattern.

   A selector defined in two files is a defect regardless of which one is
   correct today.
   ------------------------------------------------------------------------ */

/* ==========================================================================
   [WP-17.5] THE CREATE HUB

   The screen where this product's differentiator lives, and until this sprint
   it opened on a field called "כותרת" with the import panel not rendered at
   all. The hierarchy below is the argument of the whole sprint: the paste box
   is the hero, and the two other doors sit beside it as alternatives.

   Everything reads from the token layer. No new colour, no new radius and no
   new spacing step - the Warm Table system was not the problem.
   ========================================================================== */

.create-hub__head {
  max-width: var(--measure-read);
  margin-inline: auto;
  padding-block: var(--space-7) var(--space-5);
  text-align: center;
}

.create-hub__title {
  margin: 0 0 var(--space-2);
  font: var(--type-display);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-strong);
}

.create-hub__sub {
  margin: 0;
  font: var(--type-body);
  color: var(--color-text-muted);
}

.create-hub {
  max-width: var(--measure-read);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* -- the hero ------------------------------------------------------------ */

/*
 * The accent surface does the ranking here, not a bigger font. A member
 * scanning this screen has to be able to tell at one glance which of the three
 * doors the product is actually good at.
 */
.create-hero {
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: linear-gradient(160deg, var(--rh-clay-50), var(--rh-clay-100));
  border: 1px solid var(--rh-clay-200);
  box-shadow: var(--shadow-1);
}

.create-hero__eyebrow {
  margin: 0 0 var(--space-2);
  font: var(--type-label);
  color: var(--color-accent-text);
}

.create-hero__title {
  margin: 0 0 var(--space-2);
  font: var(--type-title);
  color: var(--color-text-strong);
}

.create-hero__lead {
  margin: 0 0 var(--space-5);
  font: var(--type-body);
  color: var(--color-text);
  max-width: 52ch;
}

.create-hero__form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-start;
}

.create-hero__field {
  flex: 1 1 260px;
  min-width: 0;
}

.create-hero__field input {
  width: 100%;
  min-height: var(--tap-min);
  font: var(--type-body);
}

.create-hero__form .btn {
  min-height: var(--tap-min);
  flex: 0 0 auto;
}

/*
 * The supported sources, named rather than left to be guessed.
 *
 * The chip that lights up while the member types confirms the link was
 * understood BEFORE they press anything - the difference between "paste and
 * hope" and "paste and see".
 */
.create-hero__platforms {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.create-hero__platforms li {
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--rh-clay-200);
  background: var(--color-surface);
  font: var(--type-caption);
  color: var(--color-text-muted);
  transition: background var(--speed) var(--ease-out), color var(--speed) var(--ease-out);
}

/* Colour is not the only carrier: the weight and the border move with it. */
.create-hero__platforms li.is-detected {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
  font-weight: var(--weight-bold);
}

.create-hero__detected,
.create-hero__status {
  margin: var(--space-3) 0 0;
  font: var(--type-body-sm);
  min-height: 1.4em;
}

.create-hero__detected { color: var(--color-accent-text); }
.create-hero__status { color: var(--color-text-muted); }

.create-hero__demo {
  margin-top: var(--space-4);
  font: var(--type-caption);
  color: var(--color-text-muted);
}

.create-hero__demo summary {
  cursor: pointer;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
}

/* -- the two other doors ------------------------------------------------- */

.create-choices {
  display: grid;
  gap: var(--space-4);
}

@media (min-width: 720px) {
  .create-choices { grid-template-columns: 1fr 1fr; }
}

.create-choice {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-5);
  text-align: start;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  cursor: pointer;
  transition: border-color var(--speed) var(--ease-out),
              transform var(--speed) var(--ease-out),
              box-shadow var(--speed) var(--ease-out);
}

.create-choice:hover {
  border-color: var(--color-accent);
  transform: translateY(var(--hover-lift));
  box-shadow: var(--shadow-2);
}

.create-choice:active { transform: scale(var(--press-scale)); }

.create-choice__icon {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.create-choice__body { display: flex; flex-direction: column; gap: var(--space-1); }
.create-choice__title { font: var(--type-subhead); color: var(--color-text-strong); }
.create-choice__sub { font: var(--type-body-sm); color: var(--color-text-muted); }

/* -- the state machine --------------------------------------------------- */

/*
 * `display:none` rather than `visibility` or a height animation: the form
 * carries a dozen focusable controls, and a hidden-but-focusable form is a tab
 * order that walks into a screen nobody can see.
 *
 * The attribute is on the SERVER-rendered shell, so the first paint is already
 * correct - there is no frame in which the bare form is visible.
 */
.page-editor--hub[data-create-step="choose"] .create-hub__panel { display: none; }

/* Choosing a door hides the hub and shows the panel it belongs to. */
.page-editor--hub[data-create-step="manual"] .create-hub,
.page-editor--hub[data-create-step="ai"] .create-hub { display: none; }

.page-editor--hub[data-create-step="manual"] #ai-panel { display: none; }

/* -- the duplicate state ------------------------------------------------- */

/*
 * Product decision 7: a link that already has a recipe is a USEFUL state, not
 * a 409. So it is styled as information with a destination, in the accent
 * family rather than the danger one - nothing went wrong here.
 */
.import-duplicate {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  border: 1px solid var(--color-accent);
}

.import-duplicate__title {
  margin: 0 0 var(--space-3);
  font: var(--type-label);
  color: var(--color-accent-text);
}

.import-duplicate__card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: inherit;
  background: var(--color-surface-sunken);
}

.import-duplicate__card:hover { background: var(--color-accent-soft); }

.import-duplicate__thumb {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: var(--radius-xs);
  flex: 0 0 auto;
}

.import-duplicate__body { display: flex; flex-direction: column; min-width: 0; }

.import-duplicate__name {
  font: var(--type-label);
  color: var(--color-text-strong);
  overflow-wrap: anywhere;
}

.import-duplicate__by { font: var(--type-caption); color: var(--color-text-muted); }
.import-duplicate__actions { margin: var(--space-4) 0 0; }

/* -- the guided paste ---------------------------------------------------- */

/*
 * Product decision 10: this is a FEATURE, not an error. It is styled as a
 * task - a heading, an explanation, a control and an action - and deliberately
 * not in the warning family, which would tell the member something went wrong
 * when the platform simply declined to describe a post.
 */
.import-guided {
  margin-top: var(--space-4);
  padding: var(--space-5);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
}

.import-guided__title {
  margin: 0 0 var(--space-2);
  font: var(--type-subhead);
  color: var(--color-text-strong);
}

.import-guided__lead {
  margin: 0 0 var(--space-4);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

.import-guided__methods {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.import-guided__method {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  font: var(--type-body-sm);
  cursor: pointer;
}

.import-guided__method:has(input:checked) {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.import-guided__text {
  width: 100%;
  font: var(--type-body-sm);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-input);
  resize: vertical;
}

.import-guided__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.import-guided__status {
  margin: var(--space-3) 0 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  min-height: 1.4em;
}

/* ==========================================================================
   [WP-17.5] THE POST COMPOSER AND SHARE KIT

   A creator tool inside a consumer page, so it is deliberately quieter than
   the recipe it sits under: one sunken surface, no accent fill, and the only
   loud control is the one that copies. The recipe is the hero of this screen
   and the composer must not compete with it.
   ========================================================================== */

.composer {
  margin: var(--space-8) 0;
  padding: var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
}

.composer__head { margin-bottom: var(--space-5); }

.composer__title {
  margin: 0 0 var(--space-1);
  font: var(--type-heading);
  color: var(--color-text-strong);
}

.composer__sub {
  margin: 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

/* -- tone ---------------------------------------------------------------- */

.composer__tones {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.composer__tone {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  font: var(--type-body-sm);
  cursor: pointer;
}

.composer__tone:has(input:checked) {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font-weight: var(--weight-bold);
}

.composer__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.composer__status {
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

/* -- the result ---------------------------------------------------------- */

.composer__result { margin-top: var(--space-6); }

/*
 * The provenance label. Section 16 requires the member to be able to tell a
 * suggestion from their own words, so it is a persistent label rather than a
 * toast that disappears - and it sits ABOVE the fields, where it is read
 * before the text is, not after.
 */
.composer__badge {
  display: inline-block;
  margin: 0 0 var(--space-4);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--color-secondary-soft);
  color: var(--color-secondary-text);
  font: var(--type-caption);
  font-weight: var(--weight-bold);
}

.composer__warnings {
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  list-style: none;
  border-inline-start: 3px solid var(--color-warning);
  background: var(--color-warning-soft);
  border-radius: var(--radius-xs);
  font: var(--type-body-sm);
  color: var(--color-warning-text);
}

.composer__warnings li + li { margin-top: var(--space-2); }

.composer__fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.composer__field { display: flex; flex-direction: column; gap: var(--space-1); }

.composer__field label {
  font: var(--type-label);
  color: var(--color-text);
}

.composer__field textarea,
.composer__preview {
  width: 100%;
  font: var(--type-body-sm);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-input);
  background: var(--color-surface);
  color: var(--color-text);
  resize: vertical;
}

.composer__subhead {
  margin: var(--space-7) 0 var(--space-3);
  font: var(--type-subhead);
  color: var(--color-text-strong);
}

/* -- the share kit ------------------------------------------------------- */

.composer__platforms {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.composer__platform {
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  cursor: pointer;
}

/* The selected tab is announced through `aria-selected`, and styled from the
   same attribute - so what a screen reader says and what a sighted reader sees
   cannot drift apart. */
.composer__platform[aria-selected="true"] {
  border-color: var(--color-accent);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-weight: var(--weight-bold);
}

.composer__kit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/*
 * The truthfulness line, and it is not fine print.
 *
 * Section 17 forbids implying that RecipeHub posted anything to a social
 * network. This sentence says plainly what the buttons above it do, and it is
 * styled to be read rather than to be dismissed.
 */
.composer__truth {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  font: var(--type-caption);
  color: var(--color-text-muted);
}

/* ==========================================================================
   [WP-17.5] THE FRONT DOOR

   The page a visitor reaches before they know anything. It carries one idea -
   paste a link, get a recipe - and the four-step loop that proves the idea is
   real. Nothing here reads the database.
   ========================================================================== */

.landing__eyebrow {
  margin: 0 0 var(--space-2);
  font: var(--type-label);
  letter-spacing: var(--tracking-wide);
  color: var(--color-accent-text);
  text-transform: none;
}

/*
 * The sources, on the front door.
 *
 * A visitor is deciding whether this handles the app they actually saw the
 * video in, and leaving that to be discovered after registration is how a
 * landing page converts nobody.
 */
.landing__sources {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

.landing__sources li {
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  font: var(--type-caption);
  color: var(--color-text-muted);
}

/* -- the loop ------------------------------------------------------------ */

.landing-loop { margin-top: var(--space-9); }

.landing-loop__title {
  margin: 0 0 var(--space-5);
  font: var(--type-heading);
  color: var(--color-text-strong);
}

/*
 * An ordered list, and not four cards in a row.
 *
 * The order is the point - each step only means anything after the one before
 * it - and a screen reader gets the sequence for free. The counter is drawn
 * rather than taken from `list-style`, because a 28px terracotta disc is the
 * thing that makes the sequence readable at a glance.
 */
.landing-loop__steps {
  display: grid;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}

@media (min-width: 720px) {
  .landing-loop__steps { grid-template-columns: repeat(4, 1fr); }
}

.landing-loop__steps li {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
}

.landing-loop__num {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-avatar);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font: var(--type-label);
}

.landing-loop__steps strong {
  font: var(--type-subhead);
  color: var(--color-text-strong);
}

.landing-loop__steps span:not(.landing-loop__num) {
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}


/* [WP-17.5] Provenance on the feed card.

   A credit, not a link. The card must never imply the member filmed the Reel,
   and a feed is not a place to put an outbound click. */
.feed-card__source {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: var(--space-2) 0 0;
  font: var(--type-caption);
  color: var(--color-text-on-media);
  opacity: .85;
}

.feed-card__source .rh-icon { width: 14px; height: 14px; flex: 0 0 auto; }

/* ==========================================================================
   [WP-17.5] ADMIN DISCOVERABILITY

   Three entry points, because one link at the end of a thirteen-item nav that
   scrolls internally is not an entry point. Product decisions 23 and 24.
   ========================================================================== */

/*
 * The header chip. Visually distinct because the management area is a
 * different room - the nav links are places inside the product, and this is a
 * door out of it.
 */
.rh-header__admin {
  min-block-size: var(--tap-min);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  min-height: var(--tap-min);
  margin-inline-start: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-label);
  text-decoration: none;
  white-space: nowrap;
}

.rh-header__admin:hover {
  background: var(--color-accent);
  color: var(--color-text-on-accent);
}

.rh-header__admin[aria-current] {
  background: var(--color-accent);
  color: var(--color-text-on-accent);
}

.rh-header__admin .rh-icon { width: 16px; height: 16px; }

/*
 * The profile card. The entry point that answers "where is it" for good - an
 * administrator reaches their own profile the way everybody does.
 */
.admin-entry {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin: var(--space-6) 0;
  padding: var(--space-5);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  border: 1px solid var(--color-accent);
}

.admin-entry__icon {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.admin-entry__body { flex: 1 1 220px; min-width: 0; }

.admin-entry__title {
  margin: 0 0 var(--space-1);
  font: var(--type-subhead);
  color: var(--color-text-strong);
}

.admin-entry__sub {
  margin: 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

.admin-entry__cta { flex: 0 0 auto; }

/* ==========================================================================
   [WP-17.5 · pass 2] THE RECIPE CARD

   The card that stopped being a TikTok card.

   The old one was a full-bleed photograph with the creator, the title, the
   metadata and a vertical like/save/share rail layered on top of it. That
   arrangement IS the TikTok argument, and an independent reviewer named it as
   the reason the product still read as a reduced clone of one.

   Three rules, and the layout is the whole of them:

     1. The photograph is bounded and carries no text. It is the element that
        makes somebody hungry, and anything written on it competes with the
        only thing on the card doing that job.
     2. The facts decide. Time, difficulty, servings and cost are a real strip
        in the reading order a cook uses.
     3. Social actions are a footer. Saving is the primary intent and is the
        only action that carries a word.
   ========================================================================== */

.rcard {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--speed) var(--ease-out), transform var(--speed) var(--ease-out);
}

.rcard:hover { box-shadow: var(--shadow-2); }

/* -- the photograph ------------------------------------------------------ */

/*
 * 4:3 rather than 4:5. A portrait crop is a video-feed shape; food is
 * photographed and eaten in landscape, and the taller frame was costing a
 * third of the screen per card for no appetite gain.
 */
.rcard__media {
  display: block;
  aspect-ratio: var(--media-ratio-hero);
  background: var(--media-placeholder);
  overflow: hidden;
}

.rcard__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--speed-slow) var(--ease-out);
}

.rcard:hover .rcard__media img { transform: scale(1.02); }

/* -- the body ------------------------------------------------------------ */

.rcard__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
}

.rcard__byline {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.rcard__creator {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-decoration: none;
}

.rcard__creator:hover { color: var(--color-accent-text); }

.rcard__avatar {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-avatar);
  object-fit: cover;
  flex: 0 0 auto;
}

.rcard__avatar--initial {
  display: grid;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-label);
}

/*
 * PROVENANCE. The one line no other recipe feed can carry, and the reason it
 * sits beside the creator rather than under the title: "who is showing me
 * this" and "where it came from" are the same question asked twice, and a
 * reader should get both answers in one glance.
 *
 * A credit, never a link. The card must not imply the member filmed the Reel,
 * and a feed is not a place to put an outbound click.
 */
.rcard__source {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-secondary-soft);
  color: var(--color-secondary-text);
  font: var(--type-caption);
}

.rcard__source .rh-icon { width: 13px; height: 13px; flex: 0 0 auto; }

.rcard__title { margin: 0; font: var(--type-heading); }

.rcard__title a {
  color: var(--color-text-strong);
  text-decoration: none;
}

.rcard__title a:hover { color: var(--color-accent-text); }

.rcard__match {
  margin: 0;
  font: var(--type-caption);
  font-weight: var(--weight-bold);
  color: var(--color-accent-text);
}

/* -- the fact strip ------------------------------------------------------ */

/*
 * The functional half of the card. Icons rather than words for the categories
 * themselves, because "45 דקות" next to a clock reads in one fixation and
 * "זמן: 45 דקות" does not.
 */
/*
 * ONE SECONDARY ROW. C-SOCIAL-3.
 *
 * Provenance, the match reason and the four facts share a rank and now share a
 * row: every value still renders, in the same words and with the same classes,
 * wrapping through one strip instead of holding four stacked lines. On the feed
 * those four lines were about eighty pixels of photograph.
 *
 * The rules and the divider went with them. A hairline above and below a strip
 * of secondary text is card grammar, and it was drawing a box around the least
 * important thing on the card.
 */
.rcard__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
}

.rcard__meta > * { margin: 0; }

.rcard__fact {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font: var(--type-body-sm);
  color: var(--color-text);
  white-space: nowrap;
}

.rcard__fact .rh-icon {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  color: var(--color-text-muted);
}

/*
 * SAFETY. Unmistakable and not dominant - the two properties product asked
 * for together in section 16.
 *
 * Warning colour, warning icon, its own line, always present when the review
 * has not happened. What it is NOT is a filled chip on the photograph, which
 * is what it used to be on every card in the product.
 */
.rcard__safety {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font: var(--type-body-sm);
  color: var(--color-warning-text);
}

.rcard__safety .rh-icon { width: 16px; height: 16px; flex: 0 0 auto; }

/* The personalised case, when the product can truthfully say it. */
.rcard__alert {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  background: var(--color-danger-soft);
  color: var(--color-danger-text);
  font: var(--type-label);
}

.rcard__why {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.rcard__why-item {
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  color: var(--color-text-muted);
  font: var(--type-caption);
}

/* -- the actions --------------------------------------------------------- */

/*
 * A footer row, off the food.
 *
 * `rh-rail` still applies, because `social.js` delegates on that contract and
 * nothing about the handlers changed - only where the row sits.
 */
.rcard__actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
  flex-direction: row;
}

.rcard__actions .rh-rail__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--tap-min);
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  background: transparent;
  border: 0;
  color: var(--color-text-muted);
  cursor: pointer;
}

.rcard__actions .rh-rail__btn:hover {
  background: var(--color-surface-sunken);
  color: var(--color-accent-text);
}

.rcard__count { font: var(--type-caption); font-variant-numeric: tabular-nums; }

/*
 * SAVE is the primary intent on a recipe card, so it is the one action that
 * looks like a button and carries a word. Everything else is an icon.
 */
.rcard__save {
  border: 1px solid var(--color-border) !important;
  padding: 0 var(--space-4) !important;
  color: var(--color-accent-text) !important;
  font: var(--type-label);
  margin-inline-end: auto;
}

.rcard__save[aria-pressed="true"] {
  background: var(--color-accent-soft) !important;
  border-color: var(--color-accent) !important;
}

.rcard__save-label { white-space: nowrap; }

.rcard__dismiss { opacity: .55; }
.rcard__dismiss:hover { opacity: 1; }

.rcard__hidden-note { display: none; }

.rcard.is-hidden .rcard__media,
.rcard.is-hidden .rcard__body { display: none; }

.rcard.is-hidden .rcard__hidden-note {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-5);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

/* -- narrow ------------------------------------------------------------- */

@media (max-width: 419px) {
  .rcard__body { padding: var(--space-4); }

  /* The label is the first thing to go, not the button. A 44px target with an
     icon still saves; a shrunken one with a word does not. */
  .rcard__save-label { display: none; }
  .rcard__save { margin-inline-end: auto; padding: 0 var(--space-3) !important; }
  .rcard__meta { gap: var(--space-1) var(--space-2); }
}

/* ==========================================================================
   [WP-17.5 · pass 2] COOKING FROM THE PAGE

   The recipe page stopped being a document and started being something you
   work from: a checklist you tick while you shop, steps you tick while your
   hands are covered in flour, and quantities for the number of people actually
   eating.

   Nothing here is persisted server-side and the page says so. Product section
   15 forbids faking persistence, and a tick that silently vanishes on another
   device is worse than no tick at all.
   ========================================================================== */

.recipe-part__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.recipe-part__head .recipe-part__title { margin: 0; }

/* -- the serving scaler -------------------------------------------------- */

.scaler {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
}

.scaler.is-changed { border-color: var(--color-accent); background: var(--color-accent-soft); }

.scaler__btn {
  min-block-size: var(--tap-min);
  width: var(--tap-min);
  height: var(--tap-min);
  border: 0;
  border-radius: var(--radius-avatar);
  background: transparent;
  color: var(--color-accent-text);
  font: var(--type-subhead);
  cursor: pointer;
  line-height: 1;
}

.scaler__btn:hover { background: var(--color-accent-soft); }

.scaler__value {
  min-width: 6ch;
  text-align: center;
  font: var(--type-label);
  color: var(--color-text-strong);
  font-variant-numeric: tabular-nums;
}

.scaler__note {
  margin: var(--space-2) 0 0;
  font: var(--type-caption);
  color: var(--color-text-muted);
}

/* -- the ingredient checklist -------------------------------------------- */

.cooklist {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-1);
}

@media (min-width: 720px) {
  .cooklist { grid-template-columns: 1fr 1fr; column-gap: var(--space-6); }
}

.cooklist__item { display: flex; }

/*
 * The native checkbox is clipped rather than hidden. `display:none` takes it
 * out of the tab order and out of the accessibility tree, which is the same
 * mistake the file picker made before WP-16 fixed it.
 */
.cooklist__tick,
.cooksteps__tick {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.cooklist__label {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  flex: 1;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-2);
  border-radius: var(--radius-xs);
  cursor: pointer;
  font: var(--type-body);
  color: var(--color-text);
  transition: background var(--speed-fast) var(--ease-out);
}

.cooklist__label:hover { background: var(--color-surface-sunken); }

/* The box, drawn rather than native, so it can carry the tick mark. */
.cooklist__label::before {
  content: '';
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-top: 3px;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-xs);
  background: var(--color-surface);
  transition: background var(--speed-fast) var(--ease-out),
              border-color var(--speed-fast) var(--ease-out);
}

.cooklist__tick:checked + .cooklist__label::before {
  background: var(--color-accent);
  border-color: var(--color-accent);
  /* A tick, drawn with a gradient rather than an image request. */
  background-image:
    linear-gradient(45deg, transparent 45%, #fff 45%, #fff 55%, transparent 55%),
    linear-gradient(-45deg, transparent 45%, #fff 45%, #fff 55%, transparent 55%);
  background-size: 60% 3px, 60% 3px;
  background-position: 22% 62%, 42% 55%;
  background-repeat: no-repeat;
}

/* Struck through AND faded: the state must not be carried by colour alone. */
.cooklist__item.is-done .cooklist__name,
.cooklist__item.is-done .cooklist__qty,
.cooklist__item.is-done .cooklist__unit {
  text-decoration: line-through;
  color: var(--color-text-muted);
}

.cooklist__qty {
  font-weight: var(--weight-bold);
  color: var(--color-text-strong);
  font-variant-numeric: tabular-nums;
}

.cooklist__note {
  margin: var(--space-3) 0 0;
  font: var(--type-caption);
  color: var(--color-text-muted);
}

/* The focus ring has to follow the label, because the input is a pixel. */
.cooklist__tick:focus-visible + .cooklist__label,
.cooksteps__tick:focus-visible + .cooksteps__label {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

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

.cooksteps {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--space-2);
  counter-reset: none;
}

.cooksteps__item { display: flex; }

.cooksteps__label {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  flex: 1;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  cursor: pointer;
  font: var(--type-body);
  transition: background var(--speed-fast) var(--ease-out),
              border-color var(--speed-fast) var(--ease-out);
}

.cooksteps__label:hover { border-color: var(--color-accent); }

.cooksteps__num {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-avatar);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-label);
  font-variant-numeric: tabular-nums;
}

.cooksteps__text { flex: 1; }

.cooksteps__time {
  flex: 0 0 auto;
  font: var(--type-caption);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.cooksteps__item.is-done .cooksteps__label {
  background: var(--color-success-soft);
  border-color: var(--color-success);
}

.cooksteps__item.is-done .cooksteps__num {
  background: var(--color-success);
  color: #fff;
}

.cooksteps__item.is-done .cooksteps__text { color: var(--color-text-muted); }

.cooksteps__progress {
  margin: 0 0 var(--space-3);
  font: var(--type-label);
  color: var(--color-success-text);
}


/* ==========================================================================
   [WP-17.5 · pass 2 · correction] THE RAIL IS NOT ABSOLUTE ANY MORE

   `.rh-rail` is declared `position:absolute; flex-direction:column` in the
   component layer, which was right for the media-first card: the rail sat over
   the photograph, bottom-right, the way a social feed has it.

   The card is recipe-first now and the rail is a footer row - but the absolute
   positioning came with it, so the buttons detached from the card entirely and
   floated in the page gutter. Three independent reviewers described the same
   thing without conferring: dark circles with no visible owner, in a colour
   family the product does not otherwise use.

   This is the correction, and it is scoped to the new component rather than
   applied to `.rh-rail` itself, so anything still using the old arrangement
   keeps it.
   ========================================================================== */

.rcard .rcard__actions.rh-rail {
  position: static;
  flex-direction: row;
  inset: auto;
  gap: var(--space-1);
  margin: 0;
}

/*
 * The rail buttons inherit a dark treatment meant for use over a photograph.
 * Off the photograph they have to read on paper.
 */
.rcard .rcard__actions .rh-rail__btn {
  background: transparent;
  color: var(--color-text-muted);
  box-shadow: none;
  width: auto;
  height: auto;
}

.rcard .rcard__actions .rh-rail__btn:hover {
  background: var(--color-surface-sunken);
  color: var(--color-accent-text);
}

.rcard .rcard__actions .rh-rail__btn[aria-pressed="true"] { color: var(--color-accent); }

/* ==========================================================================
   [WP-17.5 · pass 2 · correction] THE ADMIN PILL MUST NOT WIDEN THE PAGE

   A mobile reviewer root-caused a horizontal scrollbar that appeared on EVERY
   page for an administrator and on none for a member. The only difference is
   the "ניהול" pill in the header actions: at 390px the action row stopped
   fitting, the document became wider than the viewport, and the leftmost icon
   was clipped off the edge.

   That was this sprint's own regression, introduced by the fix for admin
   discoverability. Below 480px the pill keeps its icon and drops its label -
   `aria-label` names it either way, so nothing is lost to a screen reader.

   `overflow-x: clip` on the shell is the backstop, and it is `clip` rather
   than `hidden` deliberately: `hidden` on a scroll container would break
   `position: sticky` inside it.
   ========================================================================== */

@media (max-width: 479px) {
  .rh-header__admin-label { display: none; }

  .rh-header__admin {
    padding: var(--space-1) var(--space-2);
    margin-inline-start: 0;
  }
}

body { overflow-x: clip; }

/* ==========================================================================
   [WP-17.5 · pass 2 · correction] NATIVE CONTROLS IN A HEBREW-FIRST PRODUCT

   Every reviewer named the same set: system-blue radios inside terracotta
   pills, a blue range thumb, and textareas rendering desktop scrollbar tracks
   with arrow buttons and a resize grip on a 390px phone.

   A default control is not a neutral choice - it is another product's design
   language appearing inside yours, and blue is the one colour this palette
   does not contain.
   ========================================================================== */

.composer__tone input[type="radio"],
.import-guided__method input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  margin: 0;
  border: 2px solid var(--color-border-strong);
  border-radius: var(--radius-avatar);
  background: var(--color-surface);
  flex: 0 0 auto;
  cursor: pointer;
}

.composer__tone input[type="radio"]:checked,
.import-guided__method input[type="radio"]:checked {
  border-color: var(--color-accent);
  background: var(--color-accent);
  box-shadow: inset 0 0 0 3px var(--color-surface);
}

/* Textareas: no resize grip, no desktop scrollbar furniture. */
.composer__field textarea,
.composer__preview,
.import-guided__text {
  resize: none;
  scrollbar-width: thin;
  scrollbar-color: var(--color-border-strong) transparent;
}

/* The media tool's range control, in the product's own colour. */
.media-lab input[type="range"] {
  appearance: none;
  -webkit-appearance: none;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-border);
}

.media-lab input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-avatar);
  background: var(--color-accent);
  border: 2px solid var(--color-surface);
  box-shadow: var(--shadow-1);
  cursor: pointer;
}

.media-lab input[type="range"]::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border: 2px solid var(--color-surface);
  border-radius: var(--radius-avatar);
  background: var(--color-accent);
  cursor: pointer;
}

/* ==========================================================================
   [WP-17.5 · pass 2 · correction] DESTRUCTIVE IS NOT PRIMARY

   On the owner's own recipe, "מחיקה" was a solid red fill - the loudest
   element on the page - sitting beside "עריכה" as a quiet outline. On a phone
   it landed in the thumb arc, a few pixels above the tab bar.

   The action an owner performs is editing. The action they perform once, if
   ever, is deleting, and it does not get primary weight next to a thumb target.
   ========================================================================== */

.recipe .btn--danger {
  background: transparent;
  color: var(--color-danger-text);
  border: 1px solid var(--color-danger-soft);
  box-shadow: none;
}

.recipe .btn--danger:hover {
  background: var(--color-danger-soft);
  border-color: var(--color-danger);
}

/* ==========================================================================
   [WP-17.5 · pass 3] THE UNBREAKABLE STRING

   Two independent reviewers reported the recipe editor rendering as a BLANK
   PAGE at 390px. It does not: it renders completely, and it is 1,278px wide on
   a 390px viewport, so a viewport capture photographs an empty region of it.

   The cause was one element. `code.media-source-url` holds the imported
   source's address - and a Facebook plugin URL is 120 unbroken characters with
   no space and no hyphen, so it measured 1,059px, dragged its toolbar row to
   1,196px, and pushed the whole document sideways. Every "the mobile layout is
   broken" symptom downstream of that was this one string.

   Fixed here as a CLASS of defect rather than as one selector, because the
   next long URL will not be in `media-source-url`:

     1. the element that holds a URL wraps anywhere;
     2. the shell clips, so no single element can ever widen the document again;
     3. the mobile walk asserts `scrollWidth <= viewport`, so a regression is
        caught by a number rather than by somebody noticing a screenshot looks
        odd.
   ========================================================================== */

/*
 * Anything that displays a machine-generated address. `anywhere` rather than
 * `break-all`: a URL should break at a slash when it can, and only mid-token
 * when it must.
 */
.media-source-url,
.import-demo code,
.composer__preview,
.field__hint,
.field__error {
  min-width: 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/*
 * A flex ROW whose children may hold long text needs `min-width: 0` on the
 * children, or flex refuses to shrink them below their content width - which
 * is the actual mechanism by which one string widens a page.
 */
.toolbar > *,
.row > * { min-width: 0; }

.media-source-url {
  display: block;
  font-size: var(--text-2xs);
  line-height: var(--leading-snug);
  max-height: 3.6em;
  overflow: hidden;
}

/*
 * The backstop. `clip` rather than `hidden`: `hidden` turns the element into a
 * scroll container, which silently breaks `position: sticky` inside it - and
 * the editor's action bar is sticky.
 */
html { overflow-x: clip; }

/* ==========================================================================
   [WP-17.5 · pass 3] THE EDITOR AT 390px

   Once it stopped being 1,278px wide, the layout underneath it was still a
   desktop form narrowed rather than a mobile form designed. These are the
   three rules that make it usable with one thumb.
   ========================================================================== */

@media (max-width: 640px) {
  /* Paired selects and the four-across ingredient rows stack. */
  .page-editor .toolbar {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
  }

  /* ...except the row that carries a remove button, which stays inline: a
     full-width "remove" under every ingredient reads as the primary action. */
  .page-editor .toolbar[data-row] {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-card);
    background: var(--color-surface);
  }

  .page-editor .toolbar[data-row] .field { grid-column: 1; }
  .page-editor .toolbar[data-row] [data-remove] { grid-column: 2; grid-row: 1; }

  /* The checkbox groups were three columns of 11px targets. */
  .page-editor .u-columns { column-count: 1; }
}

/* ==========================================================================
   RECIPE DETAIL — the final arrangement. WP-17.5 pass 4.
   ==========================================================================

   Three reviewers said the same thing about the old page in three different
   ways: "everything is a rounded card", "nothing tells me what to do", "the
   most important control on a cooking product does not exist". All three are
   the same defect - the page had no hierarchy, so it had no argument.

   WHAT CARRIES HIERARCHY HERE, in order of how much work each does:

     1. ONE filled control on the screen. `.rdetail__cook` is the only thing
        painted in the accent, and it says a verb.
     2. SPACING. `--space-9` between regions, `--space-6` inside one. The eye
        groups by proximity before it reads a single word.
     3. TYPE. Display size for the dish, subhead for a section, body for the
        cooking. Three steps used consistently does more than nine borders.
     4. SURFACE, used ONCE and therefore meaning something: the cooking blocks,
        pricing and the composer keep a frame because each is genuinely a
        different kind of object. Everything else sits on the page ground.
   ========================================================================== */

.rdetail {
  max-width: var(--measure-read);
  margin-inline: auto;
}

.rdetail__state {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
}

/* -- the hero ------------------------------------------------------------ */

.rdetail__hero { margin-bottom: var(--space-9); }

.rdetail__frame {
  position: relative;
  border-radius: var(--radius-media);
  overflow: hidden;
  box-shadow: var(--shadow-media);
  background: var(--color-surface-sunken);
}

.rdetail__frame .rh-media,
.rdetail__frame .rh-plate { width: 100%; }

/*
 * Provenance, ON the photograph.
 *
 * The one line no other recipe product can carry spent three passes in muted
 * grey at the very bottom of the page. A reader who stopped after the method
 * never learned the product had done anything.
 *
 * It is the ONLY thing allowed on top of the food, it sits in a corner rather
 * than across the middle, and it carries its own scrim so it stays legible
 * over a bright dish as well as a dark one.
 */
.rdetail__origin {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-start: var(--space-3);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  max-width: calc(100% - var(--space-6));
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  background: rgba(20, 16, 13, .72);
  color: var(--color-text-on-media);
  font: var(--type-caption);
  backdrop-filter: blur(6px);
}
.rdetail__origin span { min-width: 0; overflow-wrap: anywhere; }
.rdetail__origin .rh-icon { flex: none; }

.rdetail__intro { margin-top: var(--space-6); }

.rdetail__title {
  margin: 0 0 var(--space-4);
  font: var(--type-display);
  font-size: clamp(var(--text-xl), 6vw, var(--text-2xl));
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-strong);
  text-wrap: balance;
}

.rdetail__creator {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.rdetail__creator-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  color: var(--color-text);
  text-decoration: none;
}
.rdetail__creator-link:hover .rdetail__creator-name { color: var(--color-accent-text); }
.rdetail__avatar {
  width: 40px; height: 40px;
  border-radius: var(--radius-avatar);
  object-fit: cover;
  background: var(--color-surface-sunken);
  flex: none;
}
.rdetail__avatar--initial {
  display: grid;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-label);
}
.rdetail__creator-name { font: var(--type-label); }
.rdetail__handle {
  display: block;
  font: var(--type-caption);
  color: var(--color-text-muted);
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}
.rdetail__follow {
  appearance: none;
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--color-text);
  font: var(--type-label);
  cursor: pointer;
}
.rdetail__follow[aria-pressed="true"] {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

.rdetail__lead {
  margin: 0 0 var(--space-6);
  font-family: var(--font-core);
  font-size: var(--text-md);
  line-height: var(--leading-normal);
  color: var(--color-text);
  text-wrap: pretty;
}

/*
 * THE FACT STRIP.
 *
 * A grid rather than a dot-separated run: four facts a cook weighs against
 * each other are being compared, and comparison needs columns. `auto-fit` with
 * a floor of 9rem gives four across on a desktop and two on a 320px phone,
 * which is the narrowest width the brief tests.
 *
 * The hairlines are the grid GAP over a coloured background rather than a
 * border on each cell - a border per cell doubles at every join and reads as
 * two different weights on a high-density screen.
 */
.rdetail__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1px;
  margin: 0 0 var(--space-5);
  padding: 0;
  list-style: none;
  background: var(--color-border);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.rdetail__fact {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--space-4) var(--space-3);
  background: var(--color-surface);
}
.rdetail__fact .rh-icon { color: var(--color-accent-text); }
.rdetail__fact-value {
  font: var(--type-label);
  color: var(--color-text-strong);
  overflow-wrap: anywhere;
}
.rdetail__fact-note {
  font: var(--type-caption);
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}

/*
 * SAFETY. Rank changed across two passes; semantics never did.
 *
 * It is stated on every unreviewed recipe, in words, with an icon, in the
 * warning colour - and it is a LINE rather than a filled alarm chip, because
 * every recipe in the catalogue is unreviewed and a warning that is always the
 * loudest thing on the screen carries no information at all.
 */
.rdetail__safety {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-6);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--color-success-soft);
  color: var(--color-success-text);
  font: var(--type-body-sm);
}
.rdetail__safety--warn {
  background: var(--color-warning-soft);
  color: var(--color-warning-text);
}
.rdetail__safety .rh-icon { flex: none; }
.rdetail__safety span { min-width: 0; }
.rdetail__safety-link { margin-inline-start: auto; font: var(--type-label); }

/* -- the primary action -------------------------------------------------- */

.rdetail__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

/*
 * The one filled control on the page, and it is deliberately large. Full width
 * on a phone; above the breakpoint it sizes to its content so it does not
 * become a banner.
 */
.rdetail__cook {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 56px;
  padding-inline: var(--space-7);
  font: var(--type-subhead);
  box-shadow: var(--shadow-accent);
}
.rdetail__cook .rh-icon { width: 24px; height: 24px; }

.rdetail__rail { flex-wrap: wrap; margin: 0; }

.rdetail__owner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider);
}
/* Destructive is a WORD and a colour on hover, never a permanently red button
   sitting beside two ordinary ones. */
.rdetail__delete { color: var(--color-danger-text); }
.rdetail__delete:hover { border-color: var(--color-danger); background: var(--color-danger-soft); }

/* -- the sections -------------------------------------------------------- */

/*
 * `.rpart` replaces `.recipe-part`. The difference is that the separator is
 * SPACE first and a hairline second: the old rule drew a line above every
 * block including the first two, which turned the cooking sequence into a
 * stack of framed rows.
 */
.rpart { margin: 0 0 var(--space-9); }
.rpart__title {
  margin: 0 0 var(--space-4);
  font: var(--type-heading);
  color: var(--color-text-strong);
}
.rpart__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.rpart__head .rpart__title { margin: 0; }
.rpart__note { margin: 0 0 var(--space-3); font: var(--type-body-sm); text-wrap: pretty; }
.rpart__chips { margin: var(--space-3) 0 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }

/*
 * THE FRONT DOOR, AS A SPLIT. `AuthLayout.jsx` carries the reasoning.
 *
 * Geometry taken from the demo's `.auth`: a 32dvh band of photograph above the
 * form on a phone, and from 900px a fixed form column beside a photograph that
 * fills the rest. `minmax(440px, 560px)` rather than a percentage because a form
 * column has a right width and it does not depend on how wide the monitor is.
 *
 * The demo writes `--rh-night-900`, which this product's token layer does not
 * carry. `--fl-feed-ground` is the same decision under the name this repository
 * uses for it - the near-black that is not black, already the ground of the one
 * other screen built around a photograph.
 *
 * `100dvh` and not `100vh`: this is the screen a person reaches on a phone with
 * the browser chrome still animating, and `vh` would put the submit button under
 * it.
 */
.auth {
  display: grid;
  grid-template-rows: 32dvh 1fr;
  min-block-size: 100dvh;
  background: var(--color-bg);
}

.auth__media {
  position: relative;
  overflow: hidden;
  background: var(--fl-feed-ground);
}
/*
 * `>` AND NOT A DESCENDANT, which cost a capture to learn.
 *
 * `.auth__media img` also matches the brand mark inside `.auth__media-logo`,
 * which sits in this element. At 390 the logo was stretched to 100% of a 32dvh
 * band - a 250px bowl over the photograph, with the wordmark pushed off the
 * screen beside it. The photograph is the direct child; everything else in here
 * is furniture on top of it.
 */
.auth__media > img { inline-size: 100%; block-size: 100%; object-fit: cover; object-position: center 40%; display: block; }
.auth__media-scrim { position: absolute; inset: 0; background: var(--scrim-media-top); }
/*
 * THE MARK ON THE PHOTOGRAPH CARRIES ITS OWN INK.
 *
 * The demo writes `<Logo size={30} onDark />`; this product has no on-dark
 * variant of `Brand`, it has a TOKEN for the case - `--fl-media-on`, the ink
 * `.fitem__lower` uses for the same reason on the same kind of surface.
 *
 * Without it the wordmark inherited `--color-text` and rendered near-black over
 * a photograph of bread, which is the exact defect `.fitem__lower` was given
 * its own colour to fix: text that is legible until the surface under it
 * changes. The scrim above darkens the top of the frame, so light ink is the
 * side of the contrast this is meant to be on.
 */
.auth__media-logo {
  position: absolute;
  inset-block-start: var(--space-3);
  inset-inline-start: var(--space-4);
  color: var(--fl-media-on);
  /* The photograph underneath is uncontrolled - a pale dish puts a bright
     ground behind whatever the scrim did not reach. */
  text-shadow: 0 1px 6px rgb(0 0 0 / 0.45);
}

.auth__col { display: flex; align-items: flex-start; justify-content: center; }
.auth__form { inline-size: min(400px, 100%); padding: var(--space-6) var(--gutter-screen) var(--space-8); }

/* One mark per screen. Below 900px it is on the photograph; above, in the
   column. Never both, which is what these two lines and their pair below do. */
.auth__logo { display: none; }

/*
 * P2 FROM THE ENTRY-PATH REVIEW: THE PRIMARY WAS NARROWER THAN THE ALTERNATIVE.
 *
 * The submit pill measured 80px hugging the inline-start edge, with the Google
 * button directly under it at the full 358 - so the secondary action was four
 * times the width of the primary one, and read as the more important of the
 * two. On the front door, which is the one screen where that ordering has to be
 * obvious.
 *
 * REPORTED ON THE SIGN-IN PAGE AND TRUE OF BOTH. Measured: login 80x48 ending at
 * x=374, register 91x48 ending at x=374, against a 358-wide field ending at the
 * same 374. Identical geometry - the review saw it on login because two fields
 * leave the lone pill more isolated than five do. Fixing only the page it was
 * noticed on would have left the pair disagreeing.
 *
 * Below 720 only, which is what the review asked for: at 1280 the pill is
 * already flush with the field edge and that was accepted as the alternative.
 */
@media (max-width: 719px) {
  .auth__form .form > button[type="submit"] { inline-size: 100%; }
}

.auth__title { margin: var(--space-4) 0 var(--space-1); font: var(--type-title); }
.auth__sub { margin: 0 0 var(--space-5); font: var(--type-body-sm); color: var(--color-text-muted); }

@media (min-width: 900px) {
  .auth {
    grid-template-rows: none;
    grid-template-columns: minmax(440px, 560px) 1fr;
  }
  /*
   * The picture is FIRST in the source and SECOND on the screen. Source order is
   * about what loads; reading and tab order are about what to do. A keyboard
   * arriving on this screen reaches the username field, not a decorative image.
   */
  .auth__media { order: 2; min-block-size: 100dvh; }
  .auth__col { align-items: center; min-block-size: 100dvh; }
  .auth__form { padding: var(--space-8) var(--space-7); }
  .auth__logo { display: inline-flex; }
  .auth__media-logo { display: none; }
}

/*
 * THE SOURCE PREVIEW. The gap table's "תצוגה מקדימה למקור (וואטסאפ-סטייל)".
 *
 * Geometry from `demo/Flayvo Live Demo.html` `.demo-linkprev`: an 86px square
 * of picture beside a three-line body, the whole card one link, capped at 420px
 * so it reads as an OBJECT quoted into the page rather than as a band of page
 * furniture. The demo's literals are replaced by the tokens that carry the same
 * values, which is the only difference.
 *
 * `align-items: stretch` and a fixed square: the picture matches the card's
 * height whatever the body wraps to, and a card with no thumbnail keeps the same
 * box so two recipes in a row do not disagree about how tall a source is.
 */
.srcprev { margin: var(--space-3) 0 0; }

.srcprev__card {
  display: flex;
  align-items: stretch;
  max-width: 420px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--color-surface);
  text-decoration: none;
  color: inherit;
  /* A card is a standalone control, so it clears the 48px floor by a wide
     margin - the thumbnail alone is 86. Stated rather than assumed. */
  min-block-size: var(--tap-min);
}

.srcprev__card:hover,
.srcprev__card:focus-visible { border-color: var(--color-accent); }

.srcprev__thumb {
  inline-size: 86px;
  block-size: 86px;
  flex: none;
  object-fit: cover;
  /* §3: the same crop window as every other frame that cuts a 4:5 photograph
     into something squarer. */
  object-position: center 40%;
  background: var(--media-placeholder);
}

.srcprev__thumb--empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
}

.srcprev__body {
  padding: var(--space-2) var(--space-3);
  display: grid;
  gap: 2px;
  align-content: center;
  /* Without this the grid refuses to shrink and a long title pushes the card
     past its own max-width instead of ellipsing. */
  min-inline-size: 0;
}

/* One line each. A source preview that grows to four lines has stopped being a
   preview and started being the content. */
.srcprev__title,
.srcprev__credit,
.srcprev__host {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.srcprev__title { font-size: var(--text-sm); }
.srcprev__credit { font-size: var(--text-xs); color: var(--color-text-muted); }
.srcprev__host { font-size: var(--text-2xs); color: var(--color-text-muted); }

/*
 * The two blocks a person cooks FROM are the only sections with a surface, and
 * they sit close together so the sequence reads as one continuous act rather
 * than as two sections that happen to be adjacent.
 */
.rpart--cook {
  padding: var(--space-6);
  background: var(--color-surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-1);
  font-size: var(--text-base);
  line-height: var(--leading-relaxed);
}
.rpart--cook + .rpart--cook { margin-top: calc(var(--space-4) - var(--space-9)); }

.rdetail__proof {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}
.rdetail__rating { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }

/* The comment composer belongs to the conversation, not to its own card. */
.rdetail #comment-form {
  box-shadow: none;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius-card);
  padding: var(--pad-card);
}

@media (min-width: 720px) {
  .rdetail__cook { width: auto; }
  .rdetail__actions { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .rdetail__frame .rh-media { max-height: 460px; }
}

/* ==========================================================================
   COOK MODE. Product brief section 5.
   ==========================================================================

   A full-surface layer, not a page. It covers the recipe rather than replacing
   it, so closing it returns to the exact scroll position somebody left.

   THE LAYOUT IS A COLUMN WITH A FIXED HEAD AND FOOT AND ONE GROWING MIDDLE.
   That is the whole point: the step text takes every pixel left over, and the
   two controls a wet hand aims at never move between steps.

   `100dvh` because a phone address bar collapses on scroll, and a `vh` layout
   leaves the "next" button underneath it for the first swipe.
   ========================================================================== */

.cook {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  /* head, progress, THE ONLY GROWING ROW, foot. Nothing else may grow: a row
     that both holds content and competes for leftover space is the row that
     gets undersized, and an undersized row paints over the next one. */
  grid-template-rows: auto auto auto 1fr auto;
  height: 100vh;
  height: 100dvh;
  padding: var(--space-4);
  padding-block-end: max(var(--space-4), env(safe-area-inset-bottom));
  gap: var(--space-4);
  background: var(--color-bg);
  overflow: hidden;
}

/*
 * THE SCROLLER, AND THE ONLY ONE.
 *
 * `min-height: 0` is the load-bearing declaration: a grid item defaults to
 * `min-height: auto`, which floors it at its content and makes `overflow-y`
 * unreachable - the item grows instead of scrolling, and pushes the foot off
 * the screen.
 */
.cook__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/*
 * THE STEP NEVER LEAVES THE SCREEN, AND THAT IS THE WHOLE POINT OF THE MODE.
 *
 * Pinning the foot fixed the reach problem and created a worse one: clicking a
 * drawer's `<summary>` makes the browser scroll the focused element into view,
 * and since the step card and the drawers shared one scroller, checking the
 * ingredients pushed the current step off the top. At 320px it disappeared
 * entirely. A cook opening the ingredient list lost the instruction they were
 * following - on the one screen where that must never happen.
 *
 * Sticky rather than a separate grid row: a long step still scrolls (it has to
 * - some steps are three sentences), but it never scrolls PAST, so the step
 * number and the first line stay against the top edge with the drawers moving
 * underneath.
 */
/*
 * THE STEP CARD IS ITS OWN ROW, NOT A STICKY ITEM INSIDE THE SCROLLER.
 *
 * Sticky kept the step on screen and introduced a new collision: the drawers
 * slid UNDER the card, so the מרכיבים header was sheared through the middle of
 * its glyphs at 360 and 430. All three reviewers reported it, one of them at
 * every width.
 *
 * A sticky element and the content it covers share a scroll box, and every fix
 * for that is a guess at an offset. Giving the card its own grid row removes
 * the shared box: nothing can pass beneath it because nothing is in it.
 *
 * The row is `auto` and the card is BOUNDED, which is what keeps the earlier
 * defect from returning - an unbounded auto row beside a 1fr row is exactly the
 * arrangement that produced the 340px track holding a 393px item.
 */
.cook__stage {
  max-height: 42dvh;
  overflow-y: auto;
  scrollbar-width: thin;
}

/*
 * AND THE SPACE GOES BELOW THE DRAWERS, not above them.
 *
 * `margin-block-start: auto` pushed the drawers against the foot, which put
 * ~180px of nothing between the next-step preview and the accordions on a
 * desktop. The earlier complaint - a quarter of the viewport blank BELOW the
 * accordions - was really about `הבא` sitting at 53% of the screen height with
 * emptiness under it, and pinning the foot is what fixed that. With the foot
 * pinned, trailing space inside the scroller is just the end of a short list.
 */

.cook__foot {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* The page underneath must not scroll behind the layer. */
body.is-cooking { overflow: hidden; }

.cook__bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}
.cook__exit {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--tap-min);
  padding-inline: var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  font: var(--type-body-sm);
  cursor: pointer;
  flex: none;
}
.cook__exit:hover { border-color: var(--color-accent); color: var(--color-accent-text); }
.cook__recipe {
  flex: 1;
  min-width: 0;
  margin: 0;
  font: var(--type-label);
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cook__servings { margin: 0; flex: none; font: var(--type-caption); color: var(--color-text-muted); }

.cook__progress { display: flex; flex-direction: column; gap: var(--space-2); }
.cook__track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  overflow: hidden;
}
.cook__fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  transition: width var(--speed-slow) var(--ease-out);
}
.cook__count { margin: 0; font: var(--type-caption); color: var(--color-text-muted); }

/*
 * THE STEP. Everything above and below exists to give it room.
 *
 * `clamp` on a viewport-relative middle term rather than a fixed size: this is
 * read at arm's length on a 390px phone and from across a kitchen on a laptop,
 * and one size cannot serve both.
 */
.cook__stage {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-4);
  min-height: 0;
  padding: var(--space-5);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-1);
}
.cook__stepnum {
  margin: 0;
  font: var(--type-label);
  color: var(--color-accent-text);
}
.cook__steptext {
  margin: 0;
  font-family: var(--font-core);
  font-weight: var(--weight-regular);
  font-size: clamp(var(--text-md), 4.4vw, var(--text-xl));
  line-height: var(--leading-snug);
  color: var(--color-text-strong);
  text-wrap: pretty;
  overflow-wrap: anywhere;
}
.cook__steptext:focus-visible { outline-offset: 6px; }
.cook__steptime {
  margin: 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}
.cook__done {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 48px;
  font: var(--type-body);
  cursor: pointer;
}
.cook__done input { width: 24px; height: 24px; accent-color: var(--color-accent); flex: none; }
.cook.is-stepdone .cook__stage { box-shadow: inset 0 0 0 2px var(--color-success); }

.cook__nav { display: grid; grid-template-columns: 1fr 1.4fr; gap: var(--space-3); }
.cook__navbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 56px;
  font: var(--type-subhead);
}
.cook__navbtn[disabled] { opacity: .45; cursor: not-allowed; }

.cook__drawers { display: flex; flex-direction: column; gap: var(--space-2); }
.cook__drawer {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}
.cook__summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 48px;
  padding-inline: var(--space-4);
  font: var(--type-label);
  cursor: pointer;
  list-style: none;
}
.cook__summary::-webkit-details-marker { display: none; }
.cook__summary--safety { color: var(--color-warning-text); }
.cook__ingredients {
  margin: 0;
  padding: 0 var(--space-4) var(--space-3);
  list-style: none;
}
.cook__ingredient {
  padding-block: var(--space-2);
  border-top: 1px solid var(--color-divider);
  font: var(--type-body-sm);
  overflow-wrap: anywhere;
}
.cook__ingredient.is-done { color: var(--color-text-muted); text-decoration: line-through; }
.cook__hint,
.cook__safety,
.cook__allergens { margin: 0; padding: 0 var(--space-4) var(--space-4); font: var(--type-body-sm); }
.cook__hint { color: var(--color-text-muted); font: var(--type-caption); }
.cook__truth { margin: 0; text-align: center; font: var(--type-caption); color: var(--color-text-muted); }

@media (min-width: 720px) {
  /* On a desktop the layer is a centred column rather than a full-bleed sheet:
     a step read across 1280px is a line nobody can track back from. */
  .cook { padding-inline: max(var(--space-7), calc((100vw - 760px) / 2)); }
  .cook__stage { padding: var(--space-8); }
}

/* ==========================================================================
   THE AI REVIEW. WP-17.5 pass 4.
   ==========================================================================

   THE VISUAL PROBLEM, stated so the next person does not undo the fix: this
   screen has to look like a REVIEW and not like a form, and the difference is
   not decoration. A form says "fill these in". A review says "here is what I
   found, here is how sure I am, and here are the two you should look at".

   THREE VISUAL WEIGHTS FOR FIVE STATES, deliberately:

     settled  (מהמקור, אושר)  quiet. Olive text on the page ground, no fill.
     הצעה                      neutral. Muted text, no fill.
     לבדיקה / חסר              painted. Amber, and the only fill on the panel.

   A screen where every row is highlighted has highlighted nothing. The whole
   purpose of this panel is to point at the fields that need a person, so
   exactly those are the ones that carry colour.
   ========================================================================== */

.airev {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-divider);
}

.airev__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.airev__title { margin: 0; font: var(--type-subhead); color: var(--color-text-strong); }
.airev__focus { margin: 0; font: var(--type-body-sm); color: var(--color-accent-text); }

.airev__warnings {
  margin: 0 0 var(--space-5);
  padding: var(--space-3) var(--space-4);
  list-style: none;
  border-inline-start: 3px solid var(--color-warning);
  border-radius: var(--radius-xs);
  background: var(--color-warning-soft);
  color: var(--color-warning-text);
  font: var(--type-body-sm);
}
.airev__warnings li + li { margin-top: var(--space-2); }
/* An unresolved taxonomy suggestion is information, not a warning about the
   dish. Same list, quieter, so allergen warnings keep their weight. */
.airev__warnings .airev__warning--soft { color: var(--color-text-muted); }

.airev__fields { display: flex; flex-direction: column; gap: var(--space-2); }

/*
 * One field. A row rather than a card: eight cards in a column is the "every
 * section is a rounded card" complaint again, one level down.
 */
.airev__field {
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
}
/* The two states that need a person, and only those, are painted. */
.airev__field[data-ai-state="check"],
.airev__field[data-ai-state="missing"] {
  border-color: var(--rh-amber-500);
  background: var(--color-warning-soft);
}

.airev__field-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.airev__field-name { font: var(--type-label); color: var(--color-text-strong); }

.airev__chip {
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font: var(--type-caption);
  border: 1px solid transparent;
}
/*
 * THE FIVE TRUTH STATES, and each one is visually distinct. E-1.
 *
 * The old rules had four selectors for what the brief calls five states, and
 * `source` was carrying three of them - source evidence, a value the member
 * typed and a value they confirmed all landed on one green chip. Reading the
 * panel could not tell them apart, which is what "separable" forbids.
 *
 * Colour is not the only carrier: every chip states its meaning in words, and
 * confirmed is distinguished from source evidence by weight and a filled
 * background rather than by hue alone, so the distinction survives a
 * monochrome screen and a colour vision deficiency.
 *
 * 700, not 600. Only `heebo-400` and `heebo-700` ship, and any other numeric
 * weight is synthesised by the browser into a different shape on every
 * platform. `tests/designIntegrity.test.js` caught the 600 these rules were
 * first written with, which is the guard working.
 */
.airev__chip[data-state="source_evidence"] { color: var(--color-success-text); border-color: var(--rh-olive-600); }
.airev__chip[data-state="confirmed"] {
  background: var(--color-success-soft);
  color: var(--color-success-text);
  border-color: var(--rh-olive-600);
  font-weight: 700;
}
.airev__chip[data-state="user_provided"] {
  color: var(--color-text-strong);
  border-color: var(--color-text-strong);
  font-weight: 700;
}
.airev__chip[data-state="suggestion"] { color: var(--color-text-muted); border-color: var(--color-border-strong); }
.airev__chip[data-state="missing"] {
  background: var(--rh-amber-500);
  color: var(--rh-clay-900);
  border-color: var(--rh-amber-500);
}

/*
 * Attention is a SEPARATE mark, because it is a separate question. It keeps
 * the amber the `check` state used to own, so nothing a member had learned to
 * look for changed appearance - only what it now sits beside.
 */
.airev__attention {
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font: var(--type-caption);
  background: var(--rh-amber-500);
  color: var(--rh-clay-900);
  border: 1px solid var(--rh-amber-500);
}

.airev__value {
  margin: 0 0 var(--space-1);
  font: var(--type-body);
  color: var(--color-text);
  overflow-wrap: anywhere;
  text-wrap: pretty;
}
.airev__value--empty { color: var(--color-text-muted); }
.airev__hint { margin: 0; font: var(--type-caption); color: var(--color-text-muted); }

.airev__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
/*
 * `.airev__confirmed` is gone with the element it styled. Confirmation is a
 * truth state now and the chip says it; a second sentence in the action row
 * saying the same thing is the duplicate-opening defect E-4 closed, in a
 * smaller coat.
 */
.airev__nofill { font: var(--type-caption); color: var(--color-text-muted); }

.airev__editor {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.airev__input {
  flex: 1 1 12rem;
  min-width: 0;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-input);
  background: var(--color-surface);
  color: var(--color-text);
  font: var(--type-body);
}

.airev__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider);
}
.airev__note {
  flex: 1 1 100%;
  margin: 0;
  font: var(--type-caption);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

/* ==========================================================================
   THE ADMIN SHELL. WP-17.5 pass 4, product brief section 9.
   ==========================================================================

   The rule this file is written under: VISUALLY DISTINCT FROM CONSUMER
   BROWSING, WITHOUT BECOMING AN UNRELATED APPLICATION.

   So it changes exactly three things and nothing else - the GROUND, the
   MEASURE and the DENSITY. Every colour is a Warm Table token, the accent is
   the same terracotta, the type scale is the same nine sizes, and the icons
   come from the same sprite. An administrator moving between /feed and /admin
   is in one product; they simply know which room they are in.

   NO FAKE GREEN CARDS. There is deliberately nothing on this screen that
   COULD be green: no health lights, no scores, no percentages. The only state
   colour is amber on failed imports, and only when that count is above zero.
   ========================================================================== */

.adminshell {
  /* Wider than the reading measure, because tables and filters are the work
     here. `--measure-page` is the token the settings screens already use. */
  max-width: var(--measure-page);
  margin-inline: auto;
}

.adminshell__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-7);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-card);
  background: var(--color-surface-inverse);
  color: var(--color-text-inverse);
}
.adminshell__identity { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.adminshell__identity .rh-icon { color: var(--rh-saffron-300); flex: none; }
.adminshell__title { margin: 0; font: var(--type-subhead); color: var(--color-text-inverse); }
.adminshell__who { margin: 0; font: var(--type-caption); color: var(--rh-clay-300); }

/* The way out is a first-class control. An administrator who cannot see how to
   get back to the product will keep the tab open and act in the wrong one. */
.adminshell__exit {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid rgba(255, 255, 255, .28);
  border-radius: var(--radius-pill);
  color: var(--color-text-inverse);
  text-decoration: none;
  font: var(--type-body-sm);
}
.adminshell__exit:hover {
  background: rgba(255, 255, 255, .1);
  color: var(--color-text-inverse);
}

.adminshell__section { margin-bottom: var(--space-7); }
.adminshell__section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.adminshell__heading {
  margin: 0 0 var(--space-4);
  font: var(--type-label);
  color: var(--color-text-muted);
  letter-spacing: var(--tracking-normal);
}
.adminshell__jump {
  appearance: none;
  border: 0;
  background: none;
  padding: var(--space-2);
  min-height: var(--tap-min);
  color: var(--color-accent-text);
  font: var(--type-body-sm);
  cursor: pointer;
}

/* -- the four headline counts -------------------------------------------- */

.admin-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: var(--space-3);
}
.admin-kpi {
  min-width: 0;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}
/* The one figure here that is a QUEUE - it means somebody is waiting. The
   other three are inventory, and inventory is not urgent. */
.admin-kpi--queue { border-inline-start: 3px solid var(--color-accent); }
.admin-kpi__label { margin: 0; font: var(--type-caption); color: var(--color-text-muted); }
.admin-kpi__value {
  margin: var(--space-1) 0 0;
  font: var(--type-display);
  font-size: var(--text-2xl);
  line-height: 1;
  color: var(--color-text-strong);
}
.admin-kpi__note { margin: var(--space-1) 0 0; font: var(--type-caption); color: var(--color-text-muted); }

/* -- operations ----------------------------------------------------------- */

.admin-ops {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-3);
}
.admin-op {
  min-width: 0;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}
.admin-op__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  font: var(--type-label);
  color: var(--color-text-strong);
}
.admin-op__title .rh-icon { color: var(--color-accent-text); }
.admin-op__list { margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.admin-op__list > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  min-width: 0;
}
.admin-op__list dt { margin: 0; font: var(--type-body-sm); color: var(--color-text-muted); min-width: 0; }
.admin-op__list dd {
  margin: 0;
  font: var(--type-label);
  color: var(--color-text-strong);
  text-align: end;
  overflow-wrap: anywhere;
}
/* Amber only when the count is above zero. A permanently warm row is a row
   nobody reads, and it would be warm on the day it matters too. */
.admin-op__row--warn dt,
.admin-op__row--warn dd { color: var(--color-danger-text); }

/* -- recent audit --------------------------------------------------------- */

.admin-recent {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  overflow: hidden;
}
.admin-recent__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3) var(--space-4);
  font: var(--type-body-sm);
}
.admin-recent__row + .admin-recent__row { border-top: 1px solid var(--color-divider); }
.admin-recent__action { font: var(--type-label); color: var(--color-text-strong); }
.admin-recent__target {
  flex: 1;
  min-width: 0;
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}
.admin-recent__when { color: var(--color-text-muted); font: var(--type-caption); }
.admin-recent__empty { padding: var(--space-4); color: var(--color-text-muted); font: var(--type-body-sm); }

/* -- navigation ----------------------------------------------------------- */

/*
 * Grouped, and the group label is part of the strip rather than a heading
 * above it. Eight identical buttons in a row is a list of words; the same
 * eight under "תוכן וקהילה" and "הגדרות המערכת" is a navigation.
 *
 * It scrolls horizontally on a phone and it is the ONE element here allowed
 * to - the tabs are peers and stacking them would bury the last four.
 */
.adminshell__nav {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-1);
  border-bottom: 1px solid var(--color-border);
  overflow-x: auto;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.adminshell__navgroup {
  flex: none;
  padding-inline: var(--space-3) var(--space-2);
  font: var(--type-caption);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.adminshell__navgroup:not(:first-child) {
  margin-inline-start: var(--space-3);
  border-inline-start: 1px solid var(--color-border);
}
.adminshell__nav .admin-tab { flex: none; }

/*
 * A slightly cooler, deeper ground for the whole admin surface.
 *
 * `surface: 'admin'` is already passed to the head partial, which puts
 * `data-surface="admin"` on the document - so this is one selector rather than
 * a class every element has to carry.
 */
body[data-surface="admin"] { background: var(--rh-paper-100); }

@media (max-width: 719px) {
  .adminshell__bar { padding: var(--space-3) var(--space-4); }
  .adminshell__exit { width: 100%; justify-content: center; }
  .admin-kpi__value { font-size: var(--text-xl); }
}

/* ==========================================================================
   THE CREATOR MEDIA STUDIO. WP-17.5 pass 4, product brief section 12.
   ========================================================================== */

.studio__intro {
  max-width: var(--measure-read);
  margin: 0 auto var(--space-7);
}
.studio__steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-3);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}
.studio__step {
  min-width: 0;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}
.studio__step-num {
  display: inline-grid;
  place-items: center;
  width: 28px; height: 28px;
  margin-bottom: var(--space-2);
  border-radius: var(--radius-avatar);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-label);
}
.studio__step strong { display: block; margin-bottom: 2px; font: var(--type-label); }
.studio__step span { font: var(--type-body-sm); color: var(--color-text-muted); text-wrap: pretty; }

/*
 * The honesty note, and it is the point of the section rather than a footnote.
 * Section 12 forbids implying that files are transformed or persisted, and the
 * whole studio runs in the browser.
 */
.studio__truth {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: var(--space-5) 0 0;
  padding: var(--space-3) var(--space-4);
  border-inline-start: 3px solid var(--color-success);
  border-radius: var(--radius-xs);
  background: var(--color-success-soft);
  color: var(--color-success-text);
  font: var(--type-body-sm);
  text-wrap: pretty;
}
.studio__truth .rh-icon { flex: none; margin-top: 2px; }

/* ==========================================================================
   THE MOBILE PASS. WP-17.5 pass 4, product brief section 13.
   ==========================================================================

   Measured rather than eyeballed: `QA/mobile-matrix.js` walks thirteen screens
   at 320, 360, 390 and 430 and reports horizontal overflow, controls trapped
   under the fixed tab bar, clipped dialogs and every interactive box below the
   floor `tokens.css` declares - `--tap-min`, which is 48px and was being
   contradicted by twenty-seven hardcoded 44s until 2026-08-25.

   HORIZONTAL OVERFLOW CAME BACK ZERO at all four widths and stays the gate.
   What follows are the tap targets the sweep found, each one a real control a
   thumb has to hit.
   ========================================================================== */

/*
 * THE PREFERENCE CHECKBOXES WERE 13px. This is the worst one on the list.
 *
 * `#pref-allergens` is where somebody declares a peanut allergy, and it
 * rendered as a multi-column list of 13x13 native checkboxes - about a third
 * of the floor, in three columns, on the screen whose output feeds the
 * eligibility filter that keeps food that could hospitalise them out of their
 * feed. A mis-tap there is not a cosmetic problem.
 *
 * The FIX IS THE LABEL, not the box. Growing the checkbox itself would make a
 * preferences screen look like a form for somebody with no fine motor control;
 * making the whole row the target is what every native control does, costs no
 * visual weight, and gives the same target to a mouse. The package states this
 * rule in the same words: "checkbox/radio - ה-label כולו הוא היעד, לא הריבוע".
 */
.u-columns label,
.checkgroup label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  cursor: pointer;
  /* A column break through the middle of a row leaves half a target at the
     bottom of one column and half at the top of the next. */
  break-inside: avoid;
}
.u-columns input[type="checkbox"],
.u-columns input[type="radio"],
.checkgroup input[type="checkbox"] {
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--color-accent);
}

/*
 * `btn--sm` is 36px, which is correct for a control in a dense admin table and
 * wrong for the owner actions on a recipe - עריכה, פרסום and מחיקה are three
 * consequential buttons at the end of a long reading page, aimed at with a
 * thumb.
 *
 * Raised only BELOW the breakpoint. The small size exists for a reason and the
 * reason holds on a pointer device; what does not hold is 36px on a phone.
 */
@media (max-width: 719px) {
  .btn--sm,
  .rh-btn--sm { min-height: var(--tap-min); }
}

/*
 * The "לפרטים" link in the safety line was 50x20.
 *
 * It is the route from the summary statement to the full allergen block, so it
 * is exactly the kind of small link that should not be small. It keeps its
 * inline appearance and gains a hit area.
 */
.rdetail__safety-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
}

/*
 * The composer's tone picker is a radio inside a label. The input measured
 * 16px; the LABEL is the thing anybody aims at, so it carries the floor and
 * the input stays the size a radio should be.
 */
.composer__tone { min-height: var(--tap-min); display: inline-flex; align-items: center; }

/*
 * The media studio's own controls. React owns everything inside
 * `#react-media-root` and this file must not reach into its structure, so this
 * is the one thing CSS may safely say about it: native controls get a floor.
 * The component's markup is untouched.
 */
#react-media-root input[type="range"] { min-height: var(--tap-min); }
#react-media-root input[type="file"],
#react-media-root button { min-height: var(--tap-min); }

/*
 * THE NARROWEST WIDTH. 320px is a real phone and the one the fact strip and
 * the Cook Mode navigation are tightest on.
 */
@media (max-width: 359px) {
  /* Two facts across at 320 leaves ~150px per cell, which fits "45 דקות" and
     not the timing note under it. One column, and the note survives. */
  .rdetail__facts { grid-template-columns: 1fr 1fr; }
  .rdetail__fact { padding: var(--space-3) var(--space-2); }

  /* The Cook Mode footer is two buttons and a word each; at 320 the icons are
     what push them into two lines. */
  .cook__nav { grid-template-columns: 1fr 1.3fr; gap: var(--space-2); }
  .cook__navbtn { padding-inline: var(--space-3); }

  .adminshell__bar { gap: var(--space-3); }
  .admin-kpis { grid-template-columns: 1fr 1fr; }
}

/*
 * THE TAB BAR RESERVE. WP-17.5 pass 4.
 *
 * `body:has(.rh-tabbar)` already reserves the bar's own 64px, and the sweep
 * still found a control ending 7px inside it on the create hub at 360. Exactly
 * the bar's height leaves a control flush against it - reachable in principle,
 * uncomfortable in practice, and zero margin for a browser that rounds the
 * safe-area inset differently.
 *
 * One spacing step of clearance, not a new number.
 */
body:has(.rh-tabbar) {
  padding-bottom: calc(var(--tabbar-h) + var(--space-4) + env(safe-area-inset-bottom, 0px));
}

/*
 * The last three tap targets the sweep found. WP-17.5 pass 4.
 *
 * `input[type=color]` in the media studio measured 50x27 - a native swatch,
 * which every engine sizes to its own default and none of them to 44. The
 * whole studio is a creator tool used with a pointer AND on a phone, and a
 * colour swatch is exactly the control somebody misses.
 */
#react-media-root input[type="color"] { width: 56px; height: var(--tap-min); padding: 2px; }
#react-media-root input { min-height: var(--tap-min); }

/*
 * The personalisation toggle's own label is 25px because it wraps a heading
 * and a hint below it, so the row is tall while the CLICKABLE line is not.
 */
label[for="pref-personalization"] {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding-block: var(--space-2);
  cursor: pointer;
}
label[for="pref-personalization"] input { width: 20px; height: 20px; flex: none; margin-top: 2px; }

/*
 * The skip link measured 43.6px - under the floor by rounding, on the FIRST
 * control anybody using a keyboard meets. It is only visible on focus, which
 * is precisely why nobody had noticed.
 */
.rh-skip { display: inline-flex; align-items: center; min-height: var(--tap-min); }

/* ==========================================================================
   LOOP 1 CORRECTIONS. WP-17.5 pass 4.
   ========================================================================== */

/*
 * THE PLATE FILLS ITS SLOT INSTEAD OF IMPOSING ITS OWN RATIO.
 *
 * `.rh-plate--feed` set `aspect-ratio: 4/5`, so inside a 620px-wide feed media
 * box it computed to 773px tall - and `.rcard__media` is 464px with
 * `overflow: hidden`. The bottom 309px was clipped, and the clipped part is
 * where `אין צילום של המנה` lives.
 *
 * That is a section 14 failure, not a cosmetic one: the placeholder must be
 * MARKED as not being a photograph, and on the feed - the surface where a
 * reviewer meets it most - the marking was invisible while the plate itself
 * looked exactly like a designed image.
 *
 * A plate inside a slot that already has a shape takes the slot's shape.
 */
/*
 * A plate inside a slot that already has a shape takes the slot's shape. The
 * FEED slot is a fixed height, so the plate fills it; a SHELF tile takes its
 * height from its own aspect ratio, so the plate must keep one - `height: 100%`
 * inside an auto-height link collapsed it to a 50px strip with the caption
 * printing over the monogram.
 */
.rcard__media .rh-plate {
  height: 100%;
  aspect-ratio: auto;
}
.card__media.rh-plate {
  aspect-ratio: 1 / 1;
  height: auto;
  /*
   * `.card__media` declares `display: block` for an <img>, and it wins here on
   * source order - which silently undid `.rh-plate`'s grid and left the dish's
   * initial in the top corner instead of on the plate. Restored explicitly.
   */
  display: grid;
  place-items: center;
}
.rcard__media--plate { display: block; overflow: hidden; border-radius: inherit; }

/*
 * THE DESKTOP HERO IS TWO COLUMNS.
 *
 * At 1280 the recipe was a 720px column with 280px of empty page on each side,
 * and the consequence was measurable rather than aesthetic: the primary
 * control - התחלת בישול - sat at y=959 on a 900px viewport. The one action the
 * whole redesign exists to offer was below the fold on the default desktop
 * window, which section 6 puts above it.
 *
 * Splitting the hero puts the photograph and the decision side by side, which
 * is what the extra width was for. The COOKING CONTENT stays at the reading
 * measure - Hebrew reads worse past ~72 characters and a two-column ingredient
 * list is not the fix for a wide window.
 */
@media (min-width: 1024px) {
  .rdetail { max-width: var(--measure-page); }

  .rdetail > *:not(.rdetail__hero) {
    max-width: var(--measure-read);
    margin-inline: auto;
  }

  .rdetail__hero {
    display: grid;
    grid-template-columns: 1.05fr 1fr;
    gap: var(--space-8);
    align-items: start;
  }
  .rdetail__intro { margin-top: 0; }
  /* The frame is sticky through the hero only, so the food stays with the
     decision while somebody reads the facts beside it. */
  .rdetail__frame { position: sticky; top: var(--space-6); }
  .rdetail__frame .rh-media { max-height: none; }
}

/* ==========================================================================
   LOOP 1 DEDUCTIONS. WP-17.5 pass 4.
   ==========================================================================
   Every rule below closes a finding at least two of the three independent
   reviewers reported. The shared ones came first.
   ========================================================================== */

/*
 * THE FACT STRIP HAD A HOLE IN IT.  [all three reviewers]
 *
 * The hairlines were the grid GAP showing a coloured background through, which
 * is a neat trick right up to the moment a track is empty: a recipe with three
 * facts in a four-column grid rendered a raw grey rectangle where the fourth
 * would be, "in a different background colour than its neighbours". Every
 * reviewer read it as a failed image load.
 *
 * Each cell now carries its own hairline, so an absent cell is absent rather
 * than grey. Adjacent borders double, which is why they are 1px and hairline-
 * toned rather than the border colour used elsewhere.
 */
.rdetail__facts {
  background: transparent;
  border: 0;
  border-radius: 0;
  gap: var(--space-2);
  overflow: visible;
}
.rdetail__fact {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}

/*
 * THE ALLERGEN WARNING WAS ON 100% OF CARDS.  [all three reviewers]
 *
 * THE SEMANTICS DO NOT CHANGE AND MUST NOT. It is still stated on every
 * unreviewed recipe, in words, in the warning colour, on the card and on the
 * page; "לא נבדק" still never means "אין אלרגנים"; nothing is hidden and
 * nothing moves behind a disclosure. `productSurfaces` asserts all of that and
 * those tests still pass.
 *
 * WHAT CHANGES IS WEIGHT. Every recipe in the catalogue is unreviewed, so a
 * filled amber chip at title weight appeared on ten cards out of ten - and a
 * warning that is universal carries no information, trains the reader to
 * ignore it, and makes the whole catalogue look like a grid of errors. Two of
 * the three reviewers used almost the same sentence about it.
 *
 * It becomes a quiet line: warning-toned text, no fill, no border, below the
 * title rather than beside it. On the RECIPE PAGE - where somebody is deciding
 * to cook rather than scanning - it keeps its full treatment.
 */
/*
 * AND THE EARLIER HALF OF THE SAME DEMOTION, which is why lifting one rule
 * was not enough: two blocks in this file strip this element, five hundred
 * lines apart, and the first one alone still removed the fill and the border.
 *
 * The paragraph above is kept because its reasoning holds and its measurement
 * was correct - every recipe in the catalogue is unreviewed, so this appeared
 * on ten cards out of ten. What has changed is which side of that trade the
 * design authority takes, and it now asks for a chip on the tile. The weight
 * that was objected to was "a filled amber chip AT TITLE WEIGHT"; what this
 * restores is the compact one: --text-2xs, 2px of padding, under the meta.
 */
.collection .card__warn,
.card__warn {
  display: inline-block;
  margin-top: var(--space-1);
  padding: 2px var(--space-2);
  border: 1px solid var(--color-warning);
  border-radius: var(--radius-xs);
  background: var(--color-warning-soft);
  color: var(--color-warning-text);
  font-weight: var(--weight-bold);
  font-size: var(--text-2xs);
}

/*
 * COOK MODE WAS 80-90% EMPTY.  [all three reviewers]
 *
 * "roughly 950px tall containing one line of 30px text", "~350px above and
 * ~370px below", "the single most focused screen in the product is 80% blank".
 *
 * The card was `1fr` of the layout grid and centred its content, so a short
 * step floated in the middle of a stretched box. It now sizes to its content
 * with a floor, sits at the top of its track, and the reclaimed space carries
 * two things a cook actually wants: what is coming next, and the ingredients
 * this step needs.
 */
/*
 * COUNTING CHILDREN AGAINST TRACKS WAS THE WRONG GAME, and this rule is what
 * is left of two rounds spent losing it.
 *
 * The layer went from six explicit tracks to seven when the next-step preview
 * was added, then to a seven-track rule with `1fr` on the drawers on phones -
 * and each version put a growing row next to rows that also held content. The
 * final defect was the same shape as the first: an item 393px tall inside a
 * 340px track, painting over the line below it.
 *
 * There are now FOUR tracks and only ONE of them grows, and it holds a
 * scroller rather than content. A miscount cannot produce an overlap any more,
 * because there is nothing left to miscount.
 */
.cook__stage {
  justify-content: flex-start;
  min-height: clamp(180px, 26vh, 280px);
}
.cook__steptext { font-size: clamp(var(--text-md), 5.2vw, var(--text-2xl)); }

/* The next step, previewed. Reclaimed space rather than added space. */
.cook__ahead {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-wrap: pretty;
}
.cook__ahead strong { color: var(--color-text); font-weight: var(--weight-bold); }

/*
 * A STEP WITH A STATED DURATION AND NO TIMER.  [reviewer A]
 *
 * "the single most-used affordance in cook mode and it is missing" - and it is
 * the one thing on this screen that needs no schema, no route and no request.
 */
.cook__timer {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  font: var(--type-label);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.cook__timer[data-running="true"] {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}
.cook__timer[data-done="true"] {
  border-color: var(--color-success);
  background: var(--color-success-soft);
  color: var(--color-success-text);
}

/*
 * THE TAB BAR SIGNALLED THE ACTIVE TAB WITH COLOUR ALONE.  [reviewer B]
 *
 * The desktop nav already carries an underline; the phone did not, and colour
 * alone fails for colour-vision deficiency and washes out in sunlight. The
 * product's own design rule says a state is never carried by colour alone -
 * this was the one place still breaking it.
 */
.rh-tabbar__tab { position: relative; }
.rh-tabbar__tab[aria-current="page"]::before {
  content: "";
  position: absolute;
  top: 0;
  inset-inline: 22%;
  height: 3px;
  border-radius: 0 0 var(--radius-pill) var(--radius-pill);
  background: var(--color-accent);
}
/* The create tab is a marked circle rather than an icon, so the bar would sit
   over the mark. It takes weight instead. */
.rh-tabbar__tab--create[aria-current="page"]::before { display: none; }
.rh-tabbar__tab--create[aria-current="page"] .rh-tabbar__mark {
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/*
 * THE MEDIA STUDIO SHIPPED RAW BROWSER DEFAULTS.  [all three reviewers]
 *
 * An unstyled English `Choose file / No file chosen` control, LTR, inside a
 * fully Hebrew RTL page; a native colour swatch; a native range slider with the
 * default blue thumb - "a blue that exists nowhere in the palette".
 *
 * React owns the markup inside `#react-media-root` and this stylesheet must not
 * reach into its structure (01_CLAUDE.md). Styling the CONTROLS is the one
 * thing CSS may safely do from outside, and it is enough: the file input gets
 * the product's own button treatment through `::file-selector-button`, and the
 * slider gets the accent.
 */
#react-media-root input[type="file"] {
  color: var(--color-text-muted);
  font: var(--type-body-sm);
  direction: rtl;
}
#react-media-root input[type="file"]::file-selector-button {
  margin-inline-end: var(--space-3);
  min-height: var(--tap-min);
  padding-inline: var(--space-5);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  font: var(--type-label);
  font-family: var(--font-core);
  cursor: pointer;
}
#react-media-root input[type="file"]::file-selector-button:hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}
#react-media-root input[type="range"] { accent-color: var(--color-accent); }
#react-media-root input[type="color"] {
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  cursor: pointer;
}

/*
 * THE ADMIN AUDIT PRINTED RAW DEVELOPER STRINGS.  [reviewers B and C]
 *
 * `taxonomy.deprecate`, `app_config` and full 24-character ObjectIds, into a
 * fully Hebrew interface. The Hebrew mapping is in `admin.js`; this is what
 * keeps the id from dominating the row when it is shown.
 */
.admin-recent__id {
  font-size: var(--text-2xs);
  color: var(--color-text-muted);
  direction: ltr;
  unicode-bidi: isolate;
}

/*
 * THE PRICE CARD'S LABELS AND VALUES DID NOT LINE UP.  [reviewer B]
 *
 * One `dt` against a `dd` that wrapped to four lines, so no label sat beside
 * its value and an operator could not tell which date belonged to which chain.
 * One row per chain instead.
 */
.admin-op__chain { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-3); }
.admin-op__chain dt { text-transform: capitalize; }

/*
 * THE FIVE ROW COUNTS OF A REFRESH. CP-31.
 *
 * A THIRD child in a row built as `space-between` between two, so it takes a
 * full line of its own rather than competing with the date for the same one -
 * five label-and-number pairs squeezed into the remainder of a chain row is the
 * "wrapped to four lines" defect the rule above this one exists to fix.
 *
 * It is deliberately NOT `rh-nums` at the `dd`: that class declares a value
 * left-to-right, and this value is five Hebrew labels with a number after each.
 * Each NUMBER carries the class instead. Bare integers need no isolation - the
 * neutrals that resolve to the wrong end are the separators inside a date.
 *
 * `nowrap` per segment so a count never wraps away from the word it counts,
 * while the line as a whole still breaks between segments at 320.
 *
 * THAT LAST CLAUSE WAS FALSE FROM THE DAY IT WAS WRITTEN UNTIL CP-59.
 * `CP59-01`. The five segments are adjacent elements with no whitespace text
 * node between them, and the ` · ` separator was rendered INSIDE the `nowrap`
 * span - so the only break opportunities on the line were inside the one run
 * that may not break. Measured by `QA/product-admin-ops.js` at 320, 390, 768 and
 * 1280 - and NOT by `QA/product-i18n-dataset.js`, which the first draft of this
 * note named and which would have refused the measurement: that dataset rejects
 * any surface carrying none of its thirty pathological shapes, and this card
 * draws chain ids and the product's own counters. `CP59-QA-05`.
 *
 * THE COUNTS LINE WAS 444px WIDE AND ONE LINE TALL AT ALL FOUR WIDTHS, in rows
 * 246, 316, 324 and 217px wide - 198, 128, 120 and 227px out of the row - and
 * at 320 and 390 it left the page too, 161px and 91px past the viewport. The
 * separator moved out of the segment in `OverviewPanel.jsx`; nothing here
 * changed, because `nowrap` per segment was never the defect. `M47` puts the
 * markup back and this gate goes red on it, at every width.
 *
 * AND THE FULL LINE OF ITS OWN NEVER HAPPENED EITHER. `CP59-02`, found by
 * measuring the row that was STILL too wide once `CP59-01` was fixed: all three
 * cells sat on ONE flex line, the counts `dd` starting at `top` 1059 while the
 * chain name it belongs under still had 22px to run, at every width. `M48` puts
 * both declarations back at their losing specificity and the row that asks the
 * geometry goes red at 320, 390, 768 and 1280.
 *
 * BOTH DECLARATIONS THAT WOULD HAVE MADE THAT ROW WERE OUTRANKED, and neither
 * is visible as wrong in a source read: both are present, and both say exactly
 * what they meant to say.
 *
 *   `flex-basis: 100%` here is `(0,1,0)`. `.admin-op__list dd { flex: 1 1 auto }`
 *   and `{ flex: 0 1 auto }` are `(0,1,1)`, and the shorthand sets `flex-basis`
 *   too, so this line has been overwritten with `auto` whatever order they
 *   appear in.
 *
 *   `.admin-op__chain { flex-wrap: wrap }` is `(0,1,0)`. `.admin-op__list > div
 *   { flex-wrap: nowrap }` is `(0,1,1)`, added later for the ORPHANED FIGURE in
 *   the two-cell rows - see its own note below - and a row that cannot wrap
 *   cannot give a third cell a line of its own however it is sized.
 *
 * The chain row is not one of the rows that rule was written about. Those have
 * a label and a figure and must share one edge; this one has a name, a date and
 * a five-segment count line, and its third cell is meant to be a row. So the
 * exception is written at the chain rather than the invariant weakened for
 * every row in every card: `flex: 1 1 auto` on the ordinary cells is what lets
 * a long chain name and a date share a line at all.
 *
 * It is `CP24-01`'s lesson from the other side - there a correct rule did not
 * REACH the surfaces that needed it; here two correct rules reach their element
 * and are outranked before they arrive.
 */
.admin-op__counts {
  margin: 0;
  font-size: var(--text-2xs);
  color: var(--color-text-muted);
  text-align: start;
}
.admin-op__list > div.admin-op__chain { flex-wrap: wrap; }
.admin-op__list dd.admin-op__counts { flex-basis: 100%; }
.admin-op__count { white-space: nowrap; }

/*
 * THE CREATE HUB HAD DEAD SPACE AND A CLIPPED FOOTER LINE.  [reviewer C]
 */
.create-hero { padding-bottom: var(--space-5); }
.create-hub__panel:last-child { margin-bottom: var(--space-6); }

/*
 * THE AI FAILURE STATE. Loop 1, reviewer C: "a failure styled as a footnote".
 *
 * Two kinds, and they are different facts rather than different wordings. A
 * throttle means WAIT; an extraction failure means TRY SOMETHING ELSE. Reporting
 * a throttle as a failure also made the admin screen look like it was lying,
 * because `/api/ai/status` was correctly reporting the module as ready.
 */
.airev__failure {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-4);
  padding: var(--space-4);
  border-inline-start: 3px solid var(--color-warning);
  border-radius: var(--radius-sm);
  background: var(--color-warning-soft);
  color: var(--color-warning-text);
  font: var(--type-body-sm);
  text-wrap: pretty;
}
.airev__failure[data-kind="error"] {
  border-inline-start-color: var(--color-danger);
  background: var(--color-danger-soft);
  color: var(--color-danger-text);
}
.airev__failure strong { font: var(--type-label); }
.airev__failure-safe { color: var(--color-text-muted); font: var(--type-caption); }

/*
 * The original creator of an imported recipe, beside the member who imported
 * it. Loop 1, reviewer A: the landing page promises `קרדיט ליוצר המקורי` and
 * the recipe page did not visibly deliver it above the fold.
 */
.rdetail__credit {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  font: var(--type-body-sm);
  color: var(--color-text);
  text-wrap: pretty;
}
.rdetail__credit .rh-icon { flex: none; margin-top: 2px; color: var(--color-accent-text); }

/* The placeholder offers the tool that fixes it, to the owner only. */
/*
 * BELOW THE FRAME, not on top of it. Loop 2, all three reviewers: the button
 * was absolutely positioned over the plate and clipped `אין צילום של המנה`
 * mid-word - covering the one honest sentence in the tile with the control that
 * offers to fix it.
 */
.rdetail__addphoto {
  margin-top: var(--space-3);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  box-shadow: var(--shadow-2);
  color: var(--color-accent-text);
  font: var(--type-label);
  text-decoration: none;
}
.rdetail__addphoto:hover { background: var(--color-accent-soft); }

/* A recipe with no method yet says so, quietly, where the CTA would have been. */
.rdetail__nosteps {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-sm);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}
.rdetail__nosteps .rh-icon { flex: none; }

/* ==========================================================================
   LOOP 2 DEDUCTIONS. WP-17.5 pass 4.
   ========================================================================== */

/*
 * THREE BUTTON LEVELS, AND ONE OF THEM MEANS "UNAVAILABLE".
 * Loop 2, reviewers B and C.
 *
 * `.btn` filled itself with `--color-accent-soft` - the pale clay tint - and
 * wrote `--color-accent-text` on it. That is the same treatment a disabled
 * control gets at 50% opacity, so `צילום פריים` (the entire point of the
 * Studio), `חיפוש` (the only search trigger on Explore) and `שליחה` were
 * indistinguishable from controls that could not be pressed. One reviewer
 * measured it below 4.5:1.
 *
 * The levels are now unambiguous and systematic:
 *
 *   primary    filled terracotta, white text        one per screen
 *   secondary  surface, real border, ink text       .btn
 *   ghost      transparent, hairline border         .btn--ghost
 *   disabled   half opacity AND desaturated         [disabled]
 */
.btn:not(.btn--primary):not(.btn--ghost):not(.btn--danger) {
  background: var(--color-surface);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}
.btn:not(.btn--primary):not(.btn--ghost):not(.btn--danger):hover {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
  background: var(--color-accent-soft);
}
/* Disabled has to be unmistakable, because it is now the ONLY pale thing. */
.btn:disabled,
.btn[disabled] {
  opacity: .45;
  filter: grayscale(.4);
  cursor: not-allowed;
  box-shadow: none;
}

/*
 * THE META STRIP WRAPPED 2 + 1 AND ORPHANED A TILE. Loop 2, reviewers A and C.
 *
 * `auto-fit, minmax(9rem, 1fr)` gives two columns at 390 and leaves the third
 * fact alone on its own line beside empty space - and it burned ~400px before
 * the ingredients. Three facts belong in one row of three at every phone width;
 * they are three short values, not three paragraphs.
 */
@media (max-width: 719px) {
  .rdetail__facts { grid-template-columns: repeat(3, 1fr); }
  .rdetail__fact { padding: var(--space-3) var(--space-2); }
  .rdetail__fact-value { font-size: var(--text-xs); }
  .rdetail__fact-note { font-size: var(--text-2xs); }
  /* A fourth fact (cost) wraps to a full-width row of its own rather than
     leaving a hole - one row of three, then one row of one, both filled. */
  .rdetail__fact:nth-child(4) { grid-column: 1 / -1; }
}

/*
 * COOK MODE, AT ARM'S LENGTH. Loop 2, all three reviewers: the step card held
 * a five-word instruction in ~220pt with ~130pt of blank white inside it, and
 * the instruction was smaller than the recipe title on the previous screen.
 *
 * The type goes up hard, the card stops reserving height it does not need, and
 * the navigation is pinned to the bottom of the viewport so the two controls a
 * wet hand aims at are always in the same place.
 */
.cook__stage { min-height: 0; }
.cook__steptext {
  font-size: clamp(var(--text-lg), 7.4vw, var(--text-3xl));
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
}
.cook__stepnum { font-size: var(--text-sm); }

/* The whole row is the target, and it looks like it - reviewer B could not
   tell that the 24pt checkbox had a larger hit area, which is the same as not
   having one. */
.cook__done {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
}
.cook.is-stepdone .cook__done {
  border-color: var(--color-success);
  background: var(--color-success-soft);
  color: var(--color-success-text);
}

@media (max-width: 719px) {
  /* The step takes the room; the drawers and the truth line sit under the
     navigation rather than pushing it up the screen. */
  .cook__stage { justify-content: center; }
}

/*
 * THE ADMIN OPERATIONS ROW DID NOT SHARE A GRID WITH THE KPI ROW, and the
 * accent bar overhung its card's rounded corner. Loop 2, reviewer C.
 */
.admin-kpi { overflow: hidden; }
.admin-kpis,
.admin-ops { margin-inline: 0; }

/* The AI value wrapped under its own label and the two columns collided.
   Reviewer B. */
.admin-op__list > div { align-items: start; }
.admin-op__list dt { flex: 0 0 auto; max-width: 55%; }
.admin-op__list dd { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

/*
 * THE STUDIO'S FILE PICKER, dressed as one of ours. The input is still a real
 * `<input type="file">` - it opens the picker and takes the focus - and is
 * visually hidden inside its own label.
 */
.studio__pick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.studio__pick-input {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.studio__pick-btn {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-5);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  font: var(--type-label);
  cursor: pointer;
}
.studio__pick-input:focus-visible + .studio__pick-btn {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}
.studio__pick:hover .studio__pick-btn {
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}
.studio__pick-name { font: var(--type-body-sm); color: var(--color-text-muted); overflow-wrap: anywhere; }

/* The admin label survives on a phone - see the comment in header.ejs. */
.rh-header__admin-label { display: inline !important; }
.rh-header__admin { gap: var(--space-1); padding-inline: var(--space-3); }

/*
 * Raw identifiers are LTR inside an RTL row, and truncate at the END.
 * Loop 2, reviewer B: "the ellipsis on the wrong side for an LTR identifier".
 */
.admin-recent__id { direction: ltr; unicode-bidi: isolate; }
.admin-recent__actor { font: var(--type-body-sm); color: var(--color-accent-text); }

/*
 * The landing hero's output card. Every line is the SHAPE of a line the import
 * really writes - a quantity, a unit, a named ingredient, a numbered step, a
 * timing, the allergen state - rather than a grey rectangle standing in for
 * one. Loop 2, reviewers A and C.
 *
 * It reads no database: a guest reaches this page and no other, and a test
 * fails if a recipe id or a member name appears on it.
 */
.landing__out-list {
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font: var(--type-body-sm);
  color: var(--color-text);
}
.landing__out-list li { padding-bottom: var(--space-2); border-bottom: 1px solid var(--color-divider); }
.landing__out-q { font-weight: var(--weight-bold); font-variant-numeric: tabular-nums; color: var(--color-accent-text); }

.landing__out-step {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  font: var(--type-body-sm);
  color: var(--color-text);
  text-wrap: pretty;
}
.landing__out-num {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 22px; height: 22px;
  border-radius: var(--radius-avatar);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
}
.landing__out-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0 0 var(--space-3);
  font: var(--type-caption);
  color: var(--color-text-muted);
}
.landing__out-warn {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-xs);
  background: var(--color-warning-soft);
  color: var(--color-warning-text);
  font: var(--type-caption);
}

/*
 * The media COLUMN: the frame, and anything that belongs under it.
 *
 * Loop 2 correction: the add-photo control was moved out of the frame to stop
 * it clipping the caption, and became a third grid item of the two-column hero
 * - which pushed the whole text column below the photograph and left a void
 * beside it. The wrapper is what makes "under the frame" and "in the left
 * column" the same place.
 */
.rdetail__media { min-width: 0; }
@media (min-width: 1024px) {
  .rdetail__media { position: sticky; top: var(--space-6); }
  .rdetail__frame { position: static; }
}

/*
 * A SELECTED CHIP LOOKS SELECTED. Loop 2 correction round, reviewers B and C.
 *
 * The button rebuild removed the pale fill from every enabled control, and the
 * selected navigation chip on the profile screen had been using exactly that
 * fill to say "you are here". So the ambiguous state became an ABSENT one -
 * three identical bordered chips with nothing indicating which view was open,
 * which is a step backwards rather than a fix.
 *
 * Selection is now a filled accent-soft surface WITH an accent border, which is
 * distinct from a secondary (surface + neutral border) and from a disabled
 * control (half opacity, desaturated). The state also carries `aria-current`,
 * so it is announced rather than only drawn.
 */
.btn[aria-current="page"],
.btn[aria-current="true"],
.btn[aria-pressed="true"]:not(.btn--primary) {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-text);
  font-weight: var(--weight-bold);
}

/*
 * COOK MODE ON A PHONE STOPPED RESERVING HEIGHT IT DOES NOT NEED.
 *
 * The desktop fix was right and the phone one was not: content stayed
 * vertically centred inside a card stretched by the `1fr` track, so ~60% of the
 * tallest element on the screen was blank while the ingredients sat collapsed
 * behind an accordion. Both reviewers measured it.
 *
 * The card hugs its content and the free space goes to the drawers, which is
 * where the ingredient list a cook actually wants already lives.
 */
@media (max-width: 719px) {
  /* The free space now belongs to `.cook__body` at every width, so a phone
     needs nothing but a card that hugs its text. */
  .cook__stage { justify-content: flex-start; min-height: 0; }
}

/*
 * The admin operations value gets its own column and wraps inside it, instead
 * of orphaning the last word of `מצב הדגמה — ללא מודל חי` onto a line of its
 * own and colliding with the label. Loop 2 correction round, reviewer B.
 */
.admin-op__list > div { flex-wrap: wrap; }
.admin-op__list dt { flex: 1 1 45%; }
.admin-op__list dd { flex: 1 1 45%; text-align: end; }

/*
 * NATIVE TEXTAREA CHROME. Final round, reviewer C: "the one place the product
 * looks default rather than designed" - visible scrollbars and a resize grip on
 * the AI review, the guided import and the comment field.
 */
textarea {
  resize: vertical;
  scrollbar-width: thin;
  font-family: var(--font-core);
}

/*
 * THE GENERIC SILHOUETTE AVATAR. Every other surface in the product uses a
 * letter monogram; the profile shipped the browser's grey person glyph, and two
 * reviewers named it in the same breath as "nobody came back to this".
 */
.profile__avatar--initial,
.avatar--initial {
  display: grid;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-heading);
}

/*
 * EVERY `<summary>` IS A TAP TARGET. WP-17.5 pass 5.
 *
 * Found only once the mobile matrix stopped counting unmeasured rows as clean:
 * the recipe page's `איך חושבה העלות` disclosure measured 238x22 at 320px and
 * 348x22 at 430px - roughly half the 44px floor `tokens.css` declares, on the
 * control that opens the cost breakdown.
 *
 * It had been there through two design loops and three reviewer panels. It was
 * invisible because the ONE screen that would have reported it - recipe-detail,
 * and Cook Mode which overlays the same page - was erroring out before it was
 * measured, and an errored row passed every check silently.
 *
 * A rule on the ELEMENT rather than on each of the four call sites: a
 * disclosure is a disclosure, and the next one somebody adds should not have to
 * remember this.
 */
summary {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  cursor: pointer;
}
/* The drawers in Cook Mode already carry their own padding and floor; this
   keeps them from gaining a second one. */
.cook__summary { min-height: 48px; }

/* ==========================================================================
   THE MEMBER HOME. WP-17.5 pass 5, product brief section 9.
   ==========================================================================

   `פרופיל` is a PRIMARY destination and it opened on a settings form. A
   reviewer put it exactly: "the profile page of a product whose entire thesis
   is 'credit the creator' opens as an account-settings form... the person is
   invisible on the person's page."

   Identity, their work and their tools come first; the settings are one tap
   away in a disclosure. No new route, no new schema, and every count is a
   query rather than a statistic.
   ========================================================================== */

.memberhome {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-4);
  align-items: start;
  margin-bottom: var(--space-7);
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--color-divider);
}

.memberhome__avatar {
  width: 72px;
  height: 72px;
  border-radius: var(--radius-avatar);
  object-fit: cover;
  background: var(--color-surface-sunken);
}
/* The monogram every other surface already uses. Two reviewers named the grey
   silhouette in the same breath as "nobody came back to this". */
.memberhome__avatar--initial {
  display: grid;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
  font: var(--type-display);
  font-size: var(--text-xl);
}

.memberhome__identity { min-width: 0; }
.memberhome__name { margin: 0; font: var(--type-title); color: var(--color-text-strong); }
/* The paragraph keeps the page's direction so `start` means the same edge the
   name is on; the `<bdi>` in the markup isolates the handle's own characters.
   See the comment beside it in editProfile.ejs - `direction: ltr` here was what
   sent it to the far edge. */
.memberhome__handle {
  margin: 2px 0 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-align: start;
}
.memberhome__bio {
  margin: var(--space-3) 0 0;
  font: var(--type-body-sm);
  color: var(--color-text);
  text-wrap: pretty;
}

/*
 * FOUR COUNTS, EACH A QUERY. Section 9 forbids fake statistics, so there is
 * nothing derived here - no score, no percentage, no engagement rate, nothing
 * that would read as an analytics dashboard.
 */
.memberhome__counts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}
.memberhome__counts li { display: flex; flex-direction: column; }
.memberhome__count {
  font: var(--type-subhead);
  color: var(--color-text-strong);
  line-height: 1.1;
}
.memberhome__countlabel { font: var(--type-caption); color: var(--color-text-muted); }

/*
 * C-7: the public profile puts Follow in the slot the owner's own home puts
 * `הפרופיל הציבורי שלי` in, so the two profiles place their one action in the
 * same place. `.memberhome__actions` holds up to two controls and wraps rather
 * than shrinking them - a 44px target that has been squeezed is not a 44px
 * target, which is what the matrix measures at 320.
 */
.memberhome__public,
.memberhome__actions { grid-column: 1 / -1; justify-self: start; }
.memberhome__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* The join date is a fact about the account, not a count, so it is not in the
   row of counts and is not weighted like one. */
.memberhome__joined {
  margin: var(--space-3) 0 0;
  font: var(--type-caption);
  color: var(--color-text-muted);
}

@media (min-width: 720px) {
  .memberhome { grid-template-columns: auto 1fr auto; }
  .memberhome__public,
  .memberhome__actions { grid-column: auto; justify-self: end; align-self: center; }
}

/* -- settings, secondary ---------------------------------------------------- */

.profile-private {
  margin-top: var(--space-8);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}
/*
 * C-7 / FIND-P2-13: this was a `<summary>` and it is a `<button>`.
 *
 * The declarations below are the ones a summary needed and a button needs for
 * the opposite reason - `list-style: none` removed a marker the browser drew,
 * and the button resets remove a border and a background the browser draws
 * instead. `text-align: start` matters here and did not before: a `<button>`
 * centres its contents, which put the label and the hint in the middle of a
 * right-aligned card at every width.
 */
.profile-private__summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 56px;
  padding-block: var(--space-3);
  padding-inline: var(--space-5);
  border: 0;
  border-radius: var(--radius-card);
  background: none;
  font: var(--type-label);
  color: var(--color-text-strong);
  text-align: start;
  cursor: pointer;
}
.profile-private__summary .rh-icon { color: var(--color-text-muted); }
/*
 * The focus ring the finding asked for. A `<summary>` inherited whatever the
 * user agent drew and the card's own `overflow` clipped part of it; the ring is
 * stated here, inset so the card cannot cut it off.
 */
.profile-private__summary:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  outline-offset: 0;
}
.profile-private__hint {
  flex: 1 1 100%;
  font: var(--type-caption);
  font-weight: var(--weight-regular);
  color: var(--color-text-muted);
}
.profile-private__summary[aria-expanded='true'] {
  border-bottom: 1px solid var(--color-divider);
  border-radius: var(--radius-card) var(--radius-card) 0 0;
}
/*
 * `tabindex="-1"` earns an element a focus ring in most browsers, and this one
 * is a container being focused programmatically rather than a control being
 * operated. The ring belongs on the button that opened it.
 */
.profile-private__panel:focus { outline: none; }
.profile-private__note,
.profile-private__panel > .form,
.profile-private__panel > section { margin-inline: var(--space-5); }
.profile-private__panel > *:last-child { margin-bottom: var(--space-5); }
.admin-op__note {
  margin: var(--space-3) 0 0;
  font: var(--type-caption);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

/* ==========================================================================
   THE RECIPE OVERFLOW. WP-17.5 pass 5, product brief section 10.
   ==========================================================================
   Seven equal pills became four plus a disclosure. The destructive action now
   costs a deliberate second gesture instead of sitting beside "share".
   ========================================================================== */

.rdetail__more { position: relative; }

.rdetail__more-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-control);
  background: var(--color-surface);
  color: var(--color-text);
  font: var(--type-body-sm);
  cursor: pointer;
  list-style: none;
}
.rdetail__more-btn::-webkit-details-marker { display: none; }
.rdetail__more-btn:hover { border-color: var(--color-accent); color: var(--color-accent-text); }
.rdetail__more[open] .rdetail__more-btn {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

/*
 * The menu floats above the page above the breakpoint, and becomes an ordinary
 * block on a phone - a floating panel on a 390px screen is a panel that lands
 * half off it.
 */
.rdetail__more-menu {
  display: flex;
  flex-direction: column;
  margin-top: var(--space-2);
  padding: var(--space-1);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-2);
}

.rdetail__more-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text);
  font: var(--type-body-sm);
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}
.rdetail__more-item:hover { background: var(--color-surface-sunken); }
.rdetail__more-item .rh-icon { flex: none; color: var(--color-text-muted); }

/*
 * Destructive is separated by a rule and coloured, and it is the LAST item -
 * the position a hand travelling down a menu reaches last.
 */
.rdetail__more-item--danger {
  margin-top: var(--space-1);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-divider);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  color: var(--color-danger-text);
}
.rdetail__more-item--danger .rh-icon { color: var(--color-danger); }
.rdetail__more-item--danger:hover { background: var(--color-danger-soft); }

@media (min-width: 720px) {
  .rdetail__more-menu {
    position: absolute;
    z-index: 20;
    inset-inline-start: 0;
    top: calc(100% + var(--space-2));
    min-width: 15rem;
    margin-top: 0;
  }
}

/* The honesty claim, stated once above the list instead of on every card. */
.airev__creed {
  flex: 1 1 100%;
  margin: var(--space-2) 0 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-wrap: pretty;
}
/* A WORD, not a third colour: which gaps actually stop a publish. */
.airev__blocks {
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-danger);
  color: var(--color-danger-text);
  font: var(--type-caption);
}

/*
 * THE RECOGNISED STATE SAYS WHAT WILL HAPPEN. WP-17.5 pass 5, brief §11.
 *
 * It was a chip fill and one 11px grey line, so a member committed to the
 * import blind and the panel carried an empty band under it. It now names the
 * platform and what that platform actually yields - which needs no network
 * call, and sets the expectation the guided fallback either meets or explains.
 */
.create-hero__detected:not(:empty) {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-inline-start: 3px solid var(--color-accent);
  border-radius: var(--radius-xs);
  background: var(--color-accent-soft);
}
.create-hero__detected-name { font: var(--type-label); color: var(--color-accent-text); }
.create-hero__detected-yield { font: var(--type-body-sm); color: var(--color-text); text-wrap: pretty; }

/* The member's own work, on their own page. */
.memberhome__work { margin-bottom: var(--space-8); }
.memberhome__work-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.memberhome__work-more { margin: var(--space-4) 0 0; }

/* ==========================================================================
   PASS 5 CORRECTIONS, from the craft review.
   ========================================================================== */

/*
 * THE COMPLETION ROW ESCAPED THE CARD.
 *
 * `.cook__stage` is a grid track that shrinks when the drawers below it expand,
 * and the row inside it kept its own height - so opening the ingredients
 * accordion pushed the beige band's rounded bottom edge out through the white
 * card and onto the page. A reviewer caught it by comparing two captures of the
 * same screen in different accordion states, which is the only way it shows.
 *
 * The card owns its children again: it never shrinks below their content, and
 * it clips anything that would still cross its edge.
 */
.cook__stage {
  overflow: hidden;
  min-height: min-content;
}
.cook__done { flex: none; }

/*
 * ONE PLATE TREATMENT, NOT THREE COLOUR FAMILIES.
 *
 * The six gradients spanned clay, saffron and paper, and a reviewer saw gold
 * and blush side by side in one profile row: "three colour families for the
 * same concept reads as randomness, not editorial intent".
 *
 * The variants stay - two identical placeholders in a row read as a broken
 * asset, which is the defect they were introduced to fix - but they now vary
 * only in DEPTH within the clay family. Different enough that no two are twins,
 * close enough that they are obviously one device.
 */
.rh-plate--v0 { --plate-wash: linear-gradient(145deg, var(--rh-clay-50) 8%, var(--rh-clay-200)); }
.rh-plate--v1 { --plate-wash: linear-gradient(158deg, var(--rh-clay-100), var(--rh-clay-300)); }
.rh-plate--v2 { --plate-wash: linear-gradient(135deg, var(--rh-paper-100), var(--rh-clay-200)); }
.rh-plate--v3 { --plate-wash: linear-gradient(150deg, var(--rh-clay-50), var(--rh-clay-300)); }
.rh-plate--v4 { --plate-wash: linear-gradient(170deg, var(--rh-clay-100), var(--rh-clay-200)); }
.rh-plate--v5 { --plate-wash: linear-gradient(140deg, var(--rh-clay-200), var(--rh-clay-300)); }

/* The caption was low-contrast type crammed under the monogram. It gets its
   own band at the foot of the plate and a tone that survives every variant. */
.rh-plate__note {
  bottom: 0;
  inset-inline: 0;
  padding: var(--space-2) var(--space-3);
  background: rgba(255, 255, 255, .55);
  color: var(--rh-clay-900);
  opacity: 1;
}

/*
 * THE ALLERGEN LINE ON A CARD IS A MARKER, NOT A BANNER.
 *
 * Reported at every round by every reviewer, and the reason is arithmetic:
 * every recipe in the catalogue is unreviewed, so a full-weight warning appears
 * on 100% of cards and stops carrying information.
 *
 * THE SEMANTICS DO NOT CHANGE AND THE TESTS THAT ASSERT THEM STILL PASS. It is
 * still stated on every unreviewed card, in words, in the warning colour. What
 * changes is that on a BROWSING surface it is a quiet line, and the full-weight
 * treatment is kept for the recipe page and Cook Mode - where somebody is about
 * to act on it.
 */
.rcard__safety {
  font-size: var(--text-2xs);
  color: var(--color-text-muted);
}
.rcard__safety .rh-icon { width: 14px; height: 14px; color: var(--color-warning-text); }

/*
 * THE PROFILE HEADER WAS ALIGNED TO NOTHING. Pass 5 craft review, reviewer B:
 * `@dana` sat flush to the far left on a phone while the name and bio were
 * right-aligned, and the stat figures were right-aligned over their labels so
 * `7` hung off the end of `מתכונים מפורסמים`.
 *
 * The recipe page already gets this right - a handle sits under its name - and
 * this now matches it.
 */
.memberhome__handle { text-align: start; }
.memberhome__counts li { align-items: center; text-align: center; }

/*
 * ONE CONTENT WIDTH ON THE RECIPE PAGE.
 *
 * The hero spanned the wide measure and the cooking blocks below it the reading
 * measure, so the page visibly narrowed mid-scroll. The hero keeps the width -
 * it is a two-column composition and needs it - and the seam is removed by
 * letting the cooking blocks share the same outer bound while keeping their own
 * reading measure for the TEXT inside them.
 */
@media (min-width: 1024px) {
  .rdetail > *:not(.rdetail__hero) {
    max-width: var(--measure-page);
    padding-inline: calc((var(--measure-page) - var(--measure-read)) / 2);
  }
  .rdetail > .rpart--cook { padding-inline: var(--space-8); }
}

/*
 * NOTHING INSIDE THE SCROLLER MAY BE COMPRESSED BELOW ITS CONTENT.
 *
 * `.cook__body` is a flex column, and a flex item's default `min-height: auto`
 * is only honoured until the container runs out of room - after which the item
 * is squeezed instead of the container scrolling. The step card was squeezed
 * first, and clipping it (the first attempt at this) hid the completion row
 * rather than making room for it.
 *
 * The scroller itself is deliberately NOT covered by this rule: it is the one
 * element that must be allowed to be shorter than its content.
 */
.cook__body > * { min-height: min-content; flex: none; }
.cook__stage { overflow: visible; }

/* ==========================================================================
   NATIVE CONTROL CHROME. WP-17.5 pass 6, section 7.
   ==========================================================================

   THE DEDUCTION, in three reviewers' words across two rounds: "browser-default
   controls sitting inside a designed page". A `<select>` drawing the operating
   system's dropdown, a range input drawing the operating system's track, a
   colour well drawing the operating system's swatch. Each one is small and all
   of them together are the difference between a product and a form.

   THE RULE THIS PASS OBEYS, and it is not negotiable: the element stays what it
   is. No `<select>` becomes a listbox of divs, no range becomes a draggable
   span, no checkbox becomes a styled label with a click handler. Keyboard
   behaviour, focus order, screen-reader role and value semantics all come free
   from the real element and cost a fortune to rebuild - and every rebuild of
   them in the wild is worse than the original.

   So: `appearance: none` plus our own paint, and nothing else.
   ========================================================================== */

/*
 * SELECT. The one control that still drew OS chrome on every screen that has a
 * filter - admin, search, activity, comment search, the editor.
 *
 * The chevron is an inline SVG data URI rather than a file: it must not be a
 * network request that can fail, and it must not be a font glyph that inherits
 * the text direction. The page is `dir="rtl"`, so it sits on the LEFT, which is
 * the inline END - the same side the disclosure marker sits on everywhere else.
 */
select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: var(--space-8);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b6257' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left var(--space-4) center;
  background-size: 16px 16px;
  cursor: pointer;
}
[dir="ltr"] select { background-position: right var(--space-4) center; }
select:disabled { cursor: not-allowed; opacity: .6; }

/* The dropdown list itself is drawn by the operating system and cannot be
   styled. Its colours are inherited from the control, so they are set here
   rather than left to the OS default. */
select option { background: var(--color-surface); color: var(--color-text); }

/*
 * RANGE. Track, thumb and progress, in the product's own ramp.
 *
 * `accent-color` alone gets the fill right and leaves a 4px hairline track that
 * reads as unfinished at the width the studio scrubber runs at.
 */
.rh-range,
#react-media-root input[type="range"] {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  min-height: var(--tap-min);
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.rh-range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}
.rh-range::-moz-range-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}
.rh-range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 22px;
  height: 22px;
  margin-top: -8px;
  border: 2px solid var(--color-surface);
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: var(--shadow-1);
}
.rh-range::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border: 2px solid var(--color-surface);
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: var(--shadow-1);
}
.rh-range:focus-visible { outline: none; }
.rh-range:focus-visible::-webkit-slider-thumb { box-shadow: var(--focus-ring); }
.rh-range:focus-visible::-moz-range-thumb { box-shadow: var(--focus-ring); }
.rh-range:disabled { cursor: not-allowed; opacity: .5; }
.rh-range--sm { width: 108px; }

/*
 * COLOUR. A swatch, not an inset OS well. The native control paints a bevelled
 * rectangle with its own padding; zeroing the padding and clipping the swatch
 * gives a plain filled chip the size of a tap target.
 */
.rh-color,
#react-media-root input[type="color"] {
  appearance: none;
  -webkit-appearance: none;
  width: 44px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  cursor: pointer;
  overflow: hidden;
}
.rh-color::-webkit-color-swatch-wrapper { padding: 0; }
.rh-color::-webkit-color-swatch { border: 0; border-radius: calc(var(--radius-sm) - 1px); }
.rh-color::-moz-color-swatch { border: 0; border-radius: calc(var(--radius-sm) - 1px); }
.rh-color:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/*
 * CHECKBOX AND RADIO, everywhere they are not already custom-drawn.
 *
 * `accent-color` is the whole fix and it is deliberately the whole fix: it
 * recolours the browser's own control, which keeps the indeterminate state, the
 * high-contrast-mode rendering and the platform's focus affordance - all three
 * of which a hand-drawn checkbox loses.
 */
/* `:where()` so this carries ZERO specificity. Every control that already has
   a deliberate size or a hand-drawn box - the cook-mode tick at 24px, the
   ingredient list's own marker, the visually-hidden ones - keeps exactly what
   it had, and only the ones nobody had styled pick this up. A blanket rule at
   normal specificity would have shrunk the cook-mode checkbox from 24px to 20
   as a side effect of recolouring it. */
:where(input[type="checkbox"], input[type="radio"]) {
  accent-color: var(--color-accent);
  width: 20px;
  height: 20px;
  cursor: pointer;
}

/*
 * TEXTAREA. The resize grabber is the browser's, and it is the one piece of
 * native chrome worth keeping - a textarea a person cannot make taller is a
 * worse textarea. What was wrong was the box around it on the AI panel, which
 * did not match the fields beside it.
 */
textarea { font-family: var(--font-core); }

/*
 * THE STUDIO'S BRUSH SETTINGS, as settings rather than as two more actions.
 *
 * They sat in the same row as "צילום פריים", so a colour well and a slider
 * carried the same weight as the one control the screen exists for.
 */
.studio__brush {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-inline-start: var(--space-4);
  border-inline-start: 1px solid var(--color-divider);
}
.studio__brushctl {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
}
.studio__brushlabel { font: var(--type-label); color: var(--color-text-muted); }

@media (max-width: 719px) {
  /* On a phone the divider becomes a wrap, and a vertical rule between wrapped
     rows points at nothing. */
  .studio__brush {
    width: 100%;
    padding-inline-start: 0;
    border-inline-start: 0;
    padding-block-start: var(--space-3);
    border-block-start: 1px solid var(--color-divider);
  }
}

/* ==========================================================================
   THE MEMBER HOME, FINISHED. WP-17.5 pass 6, section 6.
   ==========================================================================

   Not a redesign - section 6 forbids one and the structure was right. These are
   the four deductions three reviewers kept writing down about the same screen.
   ========================================================================== */

/*
 * 1. THE NAVIGATION WAS THREE LOOSE BUTTONS.
 *
 * `.profile-links` had no rule of its own at all, so the three destinations
 * inherited the page's centring and sat as unrelated pills in the middle of a
 * right-aligned screen. They are one group - the three places a member's own
 * content lives - and a segmented control says that in a way three pills
 * cannot.
 *
 * They stay `<a>` elements inside a `<nav>`: this is navigation to three
 * routes, not a tab widget, and dressing it as one with `role="tab"` would
 * promise a panel that never appears.
 */
.profile-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin-bottom: var(--space-7);
  padding: var(--space-1);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  /* A segmented control is only readable as one if it hugs its content. */
  width: fit-content;
  max-width: 100%;
}
.profile-links > .btn {
  border: 0;
  background: transparent;
  box-shadow: none;
  border-radius: var(--radius-pill);
  font: var(--type-label);
}
.profile-links > .btn:hover { background: var(--color-surface); }
/* The one that leads to the work shown directly below carries the segment. */
.profile-links > .btn--secondary {
  background: var(--color-surface);
  color: var(--color-text-strong);
  box-shadow: var(--shadow-1);
}

@media (max-width: 480px) {
  /* Below this the three segments wrap and a pill-shaped container around two
     ragged rows points at nothing, so it becomes a full-width group of three
     equal columns instead. */
  .profile-links {
    width: 100%;
    border-radius: var(--radius-card);
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .profile-links > .btn { padding-inline: var(--space-2); }
}

/*
 * 2. THE HEADER'S VERTICAL RHYTHM.
 *
 * Name, handle, bio and counts each carried their own margin, so the block
 * measured taller than the avatar beside it and the action floated against
 * nothing. One spacing scale, and the identity column sets the height.
 */
.memberhome { gap: var(--space-4) var(--space-5); }
.memberhome__bio { margin-top: var(--space-2); max-width: 52ch; }
.memberhome__counts { margin-top: var(--space-4); gap: var(--space-6); }

/*
 * 3. THE ACTION WAS ALIGNED TO THE MIDDLE OF A COLUMN IT IS NOT IN.
 *
 * `align-self: center` centred it against the whole identity block, which put
 * it level with the bio rather than with the name it belongs to. It reads as
 * an action ON the identity, so it sits on the identity's first line.
 */
@media (min-width: 720px) {
  .memberhome__public { align-self: start; }
}

/*
 * 4. THE COUNTS WERE CENTRED INSIDE A RIGHT-ALIGNED HEADER.
 *
 * Centring each figure over its own label is right; centring the GROUP left it
 * floating away from the name above it.
 */
.memberhome__counts { justify-content: flex-start; }

/* ==========================================================================
   THE HEADER OVERFLOW. WP-17.5 pass 6, carried finding P5-G / P4-D.
   ==========================================================================

   Seventeen visible targets in one bar at 1440px, thirteen of them navigation
   at identical weight. Five primary destinations now sit in the bar and the
   eight secondary ones sit behind `עוד`.
   ========================================================================== */

.rh-more { position: relative; flex: 0 0 auto; }

.rh-more__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--tap-min);
  padding-inline: var(--space-3);
  border-radius: var(--radius-pill);
  color: var(--color-text-muted);
  font: var(--type-label);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.rh-more__summary::-webkit-details-marker { display: none; }
.rh-more__summary:hover { color: var(--color-accent-text); background: var(--color-surface-sunken); }
.rh-more__summary:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.rh-more[open] > .rh-more__summary { color: var(--color-accent-text); background: var(--color-surface-sunken); }
.rh-more[open] > .rh-more__summary .rh-icon { transform: rotate(180deg); }

/*
 * The panel is absolutely positioned so opening it does not reflow the bar -
 * a header that changes height when a menu opens moves the page under the
 * pointer that opened it.
 */
.rh-more__panel {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + var(--space-2));
  z-index: 60;
  display: grid;
  gap: 2px;
  min-width: 220px;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-2);
}
.rh-more__link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding-inline: var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font: var(--type-body-sm);
  text-decoration: none;
}
.rh-more__link:hover { background: var(--color-surface-sunken); color: var(--color-accent-text); }
.rh-more__link[aria-current] { background: var(--color-accent-soft); color: var(--color-accent-text); font-weight: var(--weight-bold); }
.rh-more__link .rh-icon { width: 16px; height: 16px; color: var(--color-text-muted); flex: none; }

/* The bar is hidden below 720px and the tab bar carries navigation there, so
   the disclosure goes with it rather than becoming a second phone menu. */
@media (max-width: 719px) { .rh-more { display: none; } }

/*
 * THE ADMIN CHIP WAS THE LOUDEST THING IN A CONSUMER HEADER.
 *
 * It was filled and outlined in the accent while the five destinations a member
 * actually uses were plain text - reviewer B, pass 5: "the admin ניהול pill
 * given the loudest treatment in the consumer header".
 *
 * It still has to read as a door OUT of the product rather than a place inside
 * it, so it keeps its own shape - an outline, on the page ground. The fill is
 * kept for the state that earns it: when the viewer is actually in there.
 */
.rh-header__admin {
  border-color: var(--color-border-strong);
  background: transparent;
  color: var(--color-text-muted);
}
.rh-header__admin:hover { background: var(--color-accent-soft); color: var(--color-accent-text); border-color: var(--color-accent); }

/*
 * A NUMBER AND THE WORD THAT SAYS WHAT IT COUNTS DO NOT WRAP APART.
 *
 * Used by the recipe timing note, where `הכנה 20 · בישול 90 · סה״כ 110 דקות`
 * broke at 390px into three lines with every figure orphaned from its unit.
 */
.rh-nowrap { white-space: nowrap; }
.rdetail__fact-sep { white-space: nowrap; }

/* The wake-lock line: quiet, and only present when the lock is actually held. */
.cook__wake {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin: 0;
  font: var(--type-caption);
  color: var(--color-success-text, var(--color-text-muted));
}
.cook__wake .rh-icon { width: 14px; height: 14px; }

/* ==========================================================================
   ADMIN CRAFT. WP-17.5 pass 6, section 11. No new admin feature.
   ========================================================================== */

/*
 * THE LOUDEST THING ON THE MODERATION SCREEN WAS THE IRREVERSIBLE ONE.
 *
 * Five open reports, five filled crimson `הסרת התוכן` buttons, and the two
 * ordinary outcomes - `טופל` and `דחייה` - drawn as quiet ghosts beside them.
 * The screen was pointing at the action that cannot be undone.
 *
 * The destructive control keeps its colour, because that is what says
 * "destructive", and loses its fill: an outline in the danger ramp reads as
 * dangerous without reading as recommended. `טופל` becomes the filled one,
 * because closing a handled report is what a moderator does most.
 */
.admin-report .btn--danger,
[data-remove].btn--danger {
  background: transparent;
  color: var(--color-danger-text, var(--color-danger));
  border: 1px solid var(--color-danger, currentColor);
}
.admin-report .btn--danger:hover,
[data-remove].btn--danger:hover {
  background: var(--color-danger, #a3341f);
  color: #fff;
}
[data-outcome="resolved"].btn--ghost {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
}
[data-outcome="resolved"].btn--ghost:hover { filter: brightness(.94); }

/*
 * THE OPERATIONAL CARDS STRETCHED AND THEIR CONTENT DID NOT.
 *
 * `auto-fit` makes every card as tall as the tallest, so the safety card - one
 * row - carried 100px of blank below its single figure while the import card
 * beside it held four. The rows now sit at the top of whatever height the grid
 * hands out, which is what makes four cards of different lengths read as one
 * band rather than as four boxes of different fullness.
 */
.admin-op { display: flex; flex-direction: column; }
.admin-op__list { align-content: start; flex: 1; }

/* The label and its figure are one row on a shared baseline. They were
   drifting apart on the wide cards, so a figure at the far edge had to be
   traced back to a label at the other. */
.admin-op__list > div { align-items: baseline; gap: var(--space-3); }
.admin-op__list dd { font-variant-numeric: tabular-nums; }

/* -- the studio's empty frame, section 12 ---------------------------------- */

/*
 * The canvas and its guidance are one object. The hint is a SIBLING of the
 * canvas, never content drawn into it: anything painted on the canvas is
 * captured with the frame and would end up in the creator's image.
 */
.studio__frame { position: relative; }
.studio__framehint {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  margin: 0;
  padding: var(--space-6);
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-align: center;
  text-wrap: balance;
  pointer-events: none;   /* the canvas underneath is still drawable */
}

/* The three actions and the brush settings, on one line that does not go
   ragged: the primary leads, and the settings group wraps as a unit. */
.studio__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* ==========================================================================
   PASS-6 REVIEW CORRECTIONS. Findings from reviewers A and C on the POLISH set.
   ========================================================================== */

/*
 * A DURATION IS AN ATTRIBUTE OF ITS STEP, NOT A LABEL AT THE OTHER END OF THE
 * ROW.
 *
 * `.cooksteps__text { flex: 1 }` gave the sentence every spare pixel, so a
 * short step left its `90 דקות` chip stranded ~550px away across empty space,
 * reading as an unrelated stray rather than as part of the instruction.
 */
.cooksteps__text { flex: 0 1 auto; }
.cooksteps__time { flex: none; margin-inline-start: var(--space-3); }

/*
 * THE ADMIN VALUE SITS BESIDE ITS LABEL.
 *
 * The rows were a flex pair with the value taking the remaining width, which
 * opened 300-400px canyons between `היום` and its figure - a number that has to
 * be traced back across a card to find out what it counts.
 */
.admin-op__list > div {
  display: grid;
  grid-template-columns: max-content minmax(0, auto);
  justify-content: start;
  column-gap: var(--space-3);
}
.admin-op__list dt { max-width: none; }
.admin-op__list dd { text-align: start; }

/* The three admin sections sat on three different column grids, and the drift
   read as a stagger down the page. One inset for all of them. */
.adminshell__section > .admin-kpis,
.adminshell__section > .admin-ops { padding-inline: 0; margin-inline: 0; }

/*
 * COOK MODE WAS THE SAME 760px COLUMN AT 1280 AND AT 1920.
 *
 * A reading measure is right for the step text and wrong for the whole layer:
 * on a large screen it left the drawers in a narrow strip with the rest of the
 * viewport empty, on the one surface where a cook is reading from across a
 * counter and the space is free.
 */
@media (min-width: 1280px) {
  .cook { padding-inline: max(var(--space-8), calc((100vw - 980px) / 2)); }
  /* The TEXT keeps its measure even though the layer widened - a step read
     across 980px is a line nobody can track back from. */
  .cook__steptext { max-width: 46ch; }
}

/* ==========================================================================
   CONTRAST AND CLIPPING. Reviewer B measured these; they are not judgements.
   ========================================================================== */

/*
 * THE PLACEHOLDER UNDER-READ AS A PLACEHOLDER.
 *
 * `.rh-plate__mark` was `--rh-clay-800` at 42% opacity, which measured 2.06:1
 * against its own tile - so from arm's length the plate read as a blank pink
 * rectangle rather than as a deliberate stand-in for a dish. The whole point of
 * the editorial plate is that it says something; at 2:1 it said nothing.
 *
 * Opacity is what was wrong, not the hue. The comment beside the original rule
 * is right that colour beats opacity for consistency across six washes - it
 * just did not go far enough and left the opacity in.
 */
.rh-plate__mark { opacity: .62; }
.rh-plate__note { opacity: 1; color: var(--rh-clay-900); }

/*
 * THE MOST IMPORTANT SENTENCE IN THE FAILURE STATE WAS THE FAINTEST.
 *
 * "מה שכתבתם בטופס לא השתנה." measured 4.28:1 - a narrow AA miss, on the one
 * line that tells a member their work is safe. A reassurance nobody can read
 * reassures nobody.
 */
.airev__failure-safe { color: var(--color-text); font: var(--type-body-sm); }

/*
 * THE SCROLLER CUT ITS LAST ROW WITH NO SIGN THAT THERE WAS MORE.
 *
 * With the drawers open, the safety copy continues below the fold of
 * `.cook__body` and the clip lands mid-row - which reads as severed content
 * rather than as a list that scrolls. Safety copy is the last thing that should
 * look truncated.
 *
 * A fade above the foot, drawn by the FOOT rather than by the scroller: a
 * pseudo-element inside an `overflow: auto` box scrolls away with the content
 * it is meant to be masking.
 */
.cook__foot { position: relative; }
.cook__foot::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 100%;
  height: var(--space-6);
  background: linear-gradient(to top, var(--color-bg), transparent);
  pointer-events: none;
}
/* And room at the end of the list, so the final row clears the fade instead of
   ending underneath it. */
.cook__body { padding-block-end: var(--space-5); }

/* ==========================================================================
   THE ALLERGEN NOTICE ON BROWSE SURFACES. Three reviewers, two panels.
   ==========================================================================

   THE COMPLAINT, and it is the same sentence every time: five identical
   warnings per row, in warning ink, on every card in every grid, rail and feed.
   Repetition converts an honest caveat into wallpaper - it suppresses appetite
   where a member is browsing, and it desensitises them to the same sentence on
   the recipe page, which is the one place it decides anything.

   WHAT WAS CONSIDERED AND REJECTED: removing it from cards entirely.
   `recipeCard.js` carries the reason - a member filtering by allergen must not
   be handed a grid that reads as a guarantee the detail page then quietly
   withdraws. The words have to travel with the card.

   ALSO CONSIDERED AND REJECTED: marking the seeded recipes reviewed so the
   notice appears less often. `scripts/seed.js` states why - it would make the
   demo tidier and teach the wrong lesson about the one field with a safety
   consequence.

   SO: the words stay, on every card, unchanged. What goes is the WARNING INK.
   This is the third and last demotion available - chip on the photograph, then
   a line in the card body, now a quiet line in the body's own colour. The
   information is identical and nothing is hidden; it has simply stopped
   shouting on a surface where it is telling everybody the same thing.
   ========================================================================== */

/*
 * R3-2, AND THE SECOND HALF OF F5.
 *
 * F5 reversed this demotion for `.badge--warn` and left `.card__warn`
 * standing, so the search and favourites tiles took the amber chip and the
 * EXPLORE shelves did not - they draw a different element. Measured:
 * `.badge badge--warn` at #6d4a12 on #fdf6e9 with an amber hairline on
 * /recipes and /favorites, and `.card__warn` at #6b6058 on nothing, weight
 * 400, on /explore. One sentence, two weights, three surfaces - which is the
 * exact complaint this block was written to answer, reappearing because the
 * answer was half undone.
 *
 * So it is finished rather than left. `.collection .card__warn` in
 * `components.css` is already the compact chip the review asks for - 2px
 * padding, --text-2xs, amber ground, amber hairline - and lifting the
 * flattening restores exactly that, at TILE scale rather than at the recipe
 * page full-panel scale. That distinction is what makes this answerable at
 * all: the old reasoning was right that a filled chip AT TITLE WEIGHT on ten
 * cards of ten is wallpaper, and a 12px chip under the meta line is not that.
 *
 * THE FEED KEEPS ITS OWN TREATMENT. `.fitem__safety` draws amber text with an
 * icon on the dark stage and this review approved it in as many words. A chip
 * with a pale ground on a photograph is a different problem.
 */
.feed-card__warn,
.rcard__safety {
  color: var(--color-text-muted);
  background: transparent;
  border: 0;
  font: var(--type-caption);
  font-weight: var(--weight-regular);
}
.rcard__safety .rh-icon { color: var(--color-text-muted); }

/* The badge form, used by the search and list cards. */
.card__body .badge--warn,
.rcard .badge--warn {
  background: transparent;
  border: 0;
  padding-inline: 0;
  color: var(--color-text-muted);
  font: var(--type-caption);
  font-weight: var(--weight-regular);
}

/*
 * AND ON THE RECIPE PAGE IT KEEPS ITS WEIGHT, because there it is a fact about
 * the dish somebody is about to cook rather than a repeated caption. Stated so
 * the next person does not "make it consistent" and flatten the one instance
 * that matters.
 */

/* ==========================================================================
   PANEL CONVERGENCE. The four layout findings A and C both wrote down.
   ========================================================================== */

/*
 * 1. THE RECIPE ACTION ROW ORPHANED `עוד` ONTO A LINE OF ITS OWN, on all three
 *    desktop recipe pages, while there was horizontal room for the whole row.
 *    The gap was doing it; four pills and a disclosure fit on one line at the
 *    hero's width.
 */
/*
 * REVERTED, and the revert is the finding. `flex-wrap: nowrap` stopped עוד
 * being orphaned onto its own line and clipped השלמת שלבי ההכנה - the longer
 * CTA variant on the imported-recipe state - at its own border radius. All
 * three reviewers led with it. An orphaned pill is a blemish; a primary control
 * with its label sliced off is a broken button, and the trade was the wrong way
 * round. The row wraps again, with a tighter gap so it wraps less often.
 */
@media (min-width: 720px) {
  .rdetail__actions { gap: var(--space-2); }
  .rdetail__actions > * { flex: 0 1 auto; min-width: 0; white-space: nowrap; }
}

/*
 * 2. A SCROLLER INSIDE A LAYER NEEDS TO LOOK DELIBERATE. Both reviewers read
 *    the Cook Mode body's scrollbar as "a scroller inside a scroller" appearing
 *    beside the step card. A stable gutter stops the content shifting when it
 *    appears, and a thin bar stops it reading as a nested pane.
 */
.cook__body { scrollbar-width: thin; scrollbar-gutter: stable; }

/*
 * 3. THE METHOD HAD NO MEASURE ON A DESKTOP. Full-width bordered rows with the
 *    text hugging the inline start left ~700px of white beside every short
 *    step, and the duration badge floated unanchored in it. The text now stops
 *    at a reading measure; the row keeps its width so the numbering and the
 *    surface still line up down the page.
 */
@media (min-width: 1024px) {
  .cooksteps__label { max-width: 78ch; }
  .rpart--cook .allergen-notice { max-width: 78ch; }
}

/*
 * 4. THE FALLBACK LINE ON THE CREATE HUB WAS CLIPPED BY THE TAB BAR AT 390.
 *    "אין קישור בהישג יד? נסו אחת מהדוגמאות" is the recovery path for the
 *    member who has nothing to paste - the one reader who most needs it - and
 *    it sat under the fixed bar. `body:has(.rh-tabbar)` already reserves the
 *    bar's height, but a card that is the last thing in a scroll container
 *    needs its own clearance too.
 */
@media (max-width: 719px) {
  .page-create,
  .editor-page,
  main > .page-read:last-child {
    padding-block-end: calc(var(--tabbar-h) + var(--space-6) + env(safe-area-inset-bottom, 0px));
  }
}

/*
 * F5. THE DEMOTION WAS CONSISTENT, AND CONSISTENT IN THE WRONG DIRECTION.
 *
 * What stood here flattened `.badge--warn` to muted body text on the shelves,
 * the grids and every `.badge.badge--warn`, and it was answering a real
 * complaint from two reviewers: "plain body text on explore, an amber outlined
 * pill on profile cards" - the same sentence in two weights on two surfaces,
 * which is worse than either weight alone. That reasoning is kept because it is
 * right. Only the direction it chose is reversed.
 *
 * The sentence it demoted is "אלרגנים לא נבדקו", and that is not metadata. It
 * is the one line on a recipe card that a person with a nut allergy is reading
 * the card for, and the package puts it on the media as a badge in as many
 * words: "תג התאמה + אזהרת אלרגנים על המדיה". Demoting a safety statement to the
 * weight of a cooking time made the tiles consistent with the quietest surface
 * instead of with the loudest.
 *
 * So the rule is deleted rather than narrowed. `.badge--warn` in
 * `components.css` already carries the warning ground, the warning ink and the
 * warning border; with nothing overriding it, every surface draws the same chip
 * - which is what the original complaint asked for, at the weight the content
 * deserves.
 */
/* And the recipe page keeps its weight, restated after the blanket rule above
   so "make it consistent" cannot flatten the one instance that decides
   anything. */
.rdetail__safety .badge--warn,
.allergen-notice .badge--warn {
  background: var(--color-warning-soft, var(--color-accent-soft));
  color: var(--color-warning-text);
  padding-inline: var(--space-2);
  font-weight: var(--weight-bold);
}

/*
 * A DECLARED ALLERGEN IS NOT A TAXONOMY TAG. Two reviewers, independently:
 * `מכיל שומשום` was drawn as the same grey pill as `טבעוני` and `מזרח תיכוני`,
 * inside a banner saying the list is unverified - so the one concrete fact on
 * the panel read quieter than the disclaimer about not having checked it.
 */
.cook__allergens .badge,
.allergen-notice .badge,
#safety .badge {
  background: var(--color-warning-soft, var(--color-accent-soft));
  color: var(--color-warning-text);
  border: 1px solid var(--color-warning-text);
  font-weight: var(--weight-bold);
}

/* ==========================================================================
   EMPTY SECTION STATES. WP-17.5 closure, section 4.
   ==========================================================================
   A heading over an empty list is chrome around nothing. These are the two
   places a recipe can legitimately be incomplete - an import that recovered
   identity, credit and an embed but no method - and both now say so.
   ========================================================================== */

.rpart__empty {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
}
.rpart__empty-text {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  flex: 1 1 16rem;
  min-width: 0;
  font: var(--type-body);
  color: var(--color-text);
  text-wrap: pretty;
}
.rpart__empty-text .rh-icon { flex: none; color: var(--color-text-muted); }

/* Case B: the servings figure where the scaler would have been, in the same
   slot so the head does not reflow between a scalable recipe and a prose one. */
.rpart__servings {
  margin: 0;
  font: var(--type-label);
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* ==========================================================================
   ADMIN IDENTIFIERS AND THE OPERATIONAL VALUE COLUMN. Closure pass, section 9.
   ==========================================================================

   THE REPORTED DEFECT DID NOT REPRODUCE, and the measurement is recorded here
   so nobody re-opens it on the strength of the report.

   A reviewer read `…714ac5` as the neutral ellipsis taking the RTL paragraph
   direction and flipping to the left, making the id look truncated from the
   front. Measured by reading the actual glyph boxes left-to-right on /admin,
   /users, /comments, /activity and /stats: every technical fragment on all five
   screens is already `direction: ltr; unicode-bidi: isolate`, and NONE is
   reordered - logical order and visual order are identical, `…714ac5` both
   ways. Nothing was flipping.

   The id IS truncated from the front, on purpose: `shortId` keeps the LAST six
   characters, which is what a leading ellipsis means, and the full value is in
   the `title` for a copy.

   The rule below is a GUARD rather than a fix. It states the invariant for
   every technical fragment in the admin, so the next identifier somebody adds
   inherits it instead of depending on a rule written for one class.

   AND THE GUARD ITSELF LANDED ON WORDS.  G-12.

   It listed `.admin-op__list dd` — the operational VALUE COLUMN — among the
   technical fragments, and that column is not one class. Four of its values are
   counts and two are dates; the rest are Hebrew sentences. `מצב הדגמה — ללא ספק
   חיצוני` and `מוגדר` were rendering in a paragraph declared left-to-right, so
   `text-align: end` resolved to the RIGHT and a wrapped value ragged the wrong
   edge in a right-to-left product.

   The count is a property of the STATE, not of the screen. `QA/g12-bidi.js`
   found TWO because the panel had loaded. `admin.js` answers a failed load with
   `$('.admin-op__list dd').text('לא זמין')` — so in the state an operator most
   needs to read, every value in the column is Hebrew prose declared LTR.
   Standing constraint 1, in the place a gate is least likely to look.

   So the column no longer opts every value in. A technical value opts ITSELF in
   by carrying `.rh-nums`, which the counts already had and the dates now do —
   the same class that makes them tabular. A value that is a sentence inherits
   the document, which is what it always should have done.
   ========================================================================== */

.admin-recent__id,
.admin-op__list dd.rh-nums,
.adminshell [data-identifier],
.adminshell code,
.adminshell samp {
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}

/*
 * THE OTHER HALF OF THE SAME FINDING DID REPRODUCE. "In ייבוא the numbers
 * 56 / 1 / 55 / 0 each hug the left end of their own label, so the numeric
 * column is ragged and unscannable."
 *
 * The previous pass put the grid on each ROW, which is why every row sized its
 * own columns independently. A stat table needs one grid for the whole list, so
 * the values share an edge and can be read down.
 */
/*
 * ONE COLUMN EDGE PER CARD, and the first attempt at it was worse than the
 * problem. `display: contents` on the rows put every cell into the list's grid
 * - which also swallowed the AI card's explanatory note into column one, where
 * it was clipped at the card edge, and left three labels with no values beside
 * them. A structural change to make four numbers line up is the wrong size of
 * tool.
 *
 * The rows stay rows. The value is pushed to the card's inline end with an auto
 * margin, so every row in a card shares one edge and the figures read down.
 * Tabular numerals keep them the same width as they change.
 */
.admin-op__list { display: flex; flex-direction: column; gap: var(--space-2); }
.admin-op__list > div { display: flex; align-items: baseline; gap: var(--space-3); }
.admin-op__list dt { flex: 1 1 auto; min-width: 0; margin: 0; }
.admin-op__list dd {
  flex: none;
  margin: 0;
  margin-inline-start: auto;
  text-align: end;
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   ABOVE-THE-FOLD ALLOCATION. Closure pass, section 10.
   ==========================================================================

   MEASURED, not judged. `QA/composition.js` reports where each screen's PRIMARY
   INTENT sits as a percentage of the viewport height, at 320/360/390/430:

     recipe   התחלת בישול   98-100%   at the fold edge, effectively below it
     studio   the video     105-116%  genuinely below it
     profile  the work grid  40-50%   already above it

   The profile is not an allocation problem and is left alone. Section 10 also
   says not to remove useful content to fit a viewport, so nothing below is a
   deletion - the facts get denser and the studio's explanation moves after the
   tool it explains.
   ========================================================================== */

@media (max-width: 430px) {
  /*
   * THE FOUR FACTS BECOME A ROW, not three bordered cards stacked two-wide.
   *
   * Each card carried its own border, its own padding and a value over a label,
   * so four facts cost roughly 190px of the one screen that decides whether
   * somebody cooks. They are four short facts; they read perfectly well as a
   * wrapped run with dividers, which is what they were before they were cards.
   */
  .rdetail__facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
  }
  /*
   * `padding: … 0` WAS THE DEFECT, and the divider hid it.
   *
   * Zeroing the inline padding put every label flush against the chip's
   * inline-end border while the slack sat unused on the other side - measured
   * 12.5px on one edge and 0.0 on the other, and one label 0.5px WIDER than its
   * own chip, so at 1x it read as text bursting out of its box on the primary
   * recipe surface. A reviewer measured it in two independent capture sets.
   *
   * The separator is a border on the START edge, so the padding that keeps text
   * off it has to be on the START edge too. The END edge gets the same, because
   * a chip is padded on both sides or it is not padded.
   */
  .rdetail__fact {
    flex: 0 1 auto;
    min-width: 0;
    border: 0;
    padding-block: var(--space-1);
    padding-inline: var(--space-2);
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
  }
  .rdetail__fact:first-child { padding-inline-start: 0; }
  .rdetail__fact + .rdetail__fact {
    padding-inline-start: var(--space-3);
    border-inline-start: 1px solid var(--color-divider);
  }
  /* The note is the breakdown; on a phone the value carries the fact and the
     breakdown is a tap away in the same page.

     EXCEPT where the note is not a breakdown but a CLAIM about the number
     beside it. `ממחירי סופר, הערכה חלקית` says the figure is INCOMPLETE and
     the real cost is higher, and `לפי היוצר` says whose figure it is - the
     template's own comment calls these "different claims rather than letting
     them look alike". Hiding them left a partial supermarket estimate looking
     like a complete price, and made an author's guess indistinguishable from a
     derived one, at the widths where most members read it. A qualifier is not
     a breakdown. */
  .rdetail__fact-note { display: none; }
  .rdetail__fact-note--claim { display: inline; }
}

/*
 * THE STUDIO EXPLAINED ITSELF BEFORE IT SHOWED ITSELF.
 *
 * Three explainer cards and a privacy panel filled the entire first screen, so
 * the video and the canvas - the whole tool - began at 105-116% of the viewport
 * on every phone width. A member arriving from the recipe page's
 * "הוספת תמונה מהסרטון שלכם" met a description of the thing they had just
 * chosen to use.
 *
 * The explanation is not removed; it moves BELOW the tool on a phone, where it
 * answers "how does this work" for somebody who has already seen what it is.
 * The one-sentence subtitle stays above, because that is the part that says
 * what the screen is.
 */
/*
 * ON A DESKTOP THE EXPLANATION GOES BACK ON TOP. Both fit on one screen there,
 * and the three-step framing is what tells a first-time visitor what the studio
 * is before they meet a black video frame.
 *
 * The DOCUMENT order is tool-then-explanation, which is what a phone needs and
 * what a screen reader gets. This is the one place `order` is used to disagree
 * with the document, and it is used at the width where the disagreement costs
 * nothing.
 */
.page-read:has(#react-media-root) { display: flex; flex-direction: column; }
.studio__intro { order: 0; }
#react-media-root { order: 3; }
.studio__steps { order: 1; }
.studio__truth { order: 2; }

@media (max-width: 719px) {
  /* The tool first. Everything that explains it follows it. */
  #react-media-root { order: 1; }
  .studio__steps { order: 2; }
  .studio__truth { order: 3; }
}

/*
 * THE HERO PHOTOGRAPH IS SHALLOWER ON A PHONE, and only on a phone.
 *
 * With the facts condensed, התחלת בישול still measured 94-95% of the viewport -
 * at the fold edge, which for a member is the same as below it. The remaining
 * cost is the hero itself: 4:3 at 390px is 292 vertical pixels before the title
 * has started.
 *
 * 16:10 keeps the dish large enough to want and returns ~48px, which is what
 * moves the one action this screen exists for onto the first screen. Section 10
 * names image ratio as a composition lever precisely so this does not have to
 * be paid for by deleting something.
 *
 * Desktop is untouched: there the hero is a two-column composition and the
 * photograph is doing the work.
 */
@media (max-width: 430px) {
  .rdetail__frame .rh-media--hero,
  .rdetail__frame .rh-plate--hero { aspect-ratio: 16 / 10; }
}


/* ==========================================================================
   TRACKS THAT REFUSED TO SHRINK, AND THE GATE THAT COULD NOT SEE THEM.
   ==========================================================================

   Two independent reviewers found the same thing at 320: content painted
   OUTSIDE the viewport on the inline end. Measured: `.rh-header__actions` runs
   to left = -53px on every signed-in screen, which puts the logout control
   entirely off-screen.

   WHY NO SWEEP CAUGHT IT. `html` and `body` both carry `overflow-x: clip`, so
   `document.scrollWidth` never grows past the viewport - the browser discards
   the overflow instead of making it scrollable. The responsive matrix's gate is
   `scrollWidth - clientWidth`, which is 0 here and was 0 for every one of the
   180 rows. In an RTL document, inline-end overflow is INVISIBLE to that check.
   The harness now measures element boxes against both edges as well.

   `1fr` is `minmax(auto, 1fr)`, and `auto` floors a track at its content. A
   three-segment nav of Hebrew words, or a 340px card minimum, simply refuses to
   fit 280px of content box and spills.
   ========================================================================== */

@media (max-width: 430px) {
  /*
   * The header's own actions must fit the header. `flex: none` (components.css)
   * kept the group at its natural width and the auto margin then pushed the
   * overflow off the inline end, taking the logout button with it.
   */
  .rh-header__actions { flex: 0 1 auto; min-width: 0; gap: var(--space-1); }
  .rh-header__actions > * { flex: none; }
  /* The label beside the logout glyph is the first thing worth losing; the
     control keeps its accessible name from `aria-label`. */
  .rh-header__logout-label { display: none; }
  /* The brand's shrink-and-ellipsis rule was REMOVED here, not overridden
     later: it is what clipped the R at 320, and a contradicted declaration left
     in place is a trap for whoever reads this block next. See
     `.rh-header__brand { flex: none }` below. */
}

@media (max-width: 430px) {
  /*
   * STILL 24px OVER after the label came off. The bar holds the brand, search,
   * the admin door, chat and the way out; at 320 the sum of their padding is
   * what does not fit, not any one control.
   *
   * The gutter and the inter-control gap give it back without shrinking a tap
   * target below the 44px floor - the controls keep their size, the space
   * between them does not.
   */
  .rh-header { padding-inline: var(--space-3); gap: var(--space-2); }
  .rh-header__actions { gap: 2px; }
  .rh-header__icon { padding-inline: var(--space-1); }
  .rh-header__admin { padding-inline: var(--space-2); margin-inline-start: 0; }
  .rh-btn--quiet { padding-inline: var(--space-2); }
}

/*
 * A `<fieldset>` DOES NOT SHRINK LIKE A `<div>`.
 *
 * `fieldset.u-panel` measured 18px past the viewport at 320 on the create hub's
 * AI mode - the last row the corrected overflow gate found, and one no sweep had
 * ever reported because `overflow-x: clip` hides it from `scrollWidth`.
 *
 * A fieldset's automatic minimum size is `min-content`, not 0, so a long
 * unbreakable string inside it - a URL, a Latin token - stops the box shrinking
 * to its container. `min-inline-size: 0` is the documented opt-out, and it has
 * to be stated explicitly because the usual `min-width: auto` reasoning does not
 * apply to this element.
 */
fieldset.u-panel,
.form fieldset.u-panel {
  min-inline-size: 0;
  max-inline-size: 100%;
}

/* ==========================================================================
   CONFIRMATION ROUND: THREE DEFECTS BOTH REVIEWERS COULD SEE.
   ==========================================================================

   Two of these are the SAME finding as the previous round, not fully closed.
   The strip stopped overflowing the viewport and its CELLS became narrower
   than their content, so the content escaped sideways instead - a defect that
   moved rather than one that was fixed. Worth stating plainly: "no longer
   overflows the page" was measured and true, and it was not the same claim as
   "the control contains its own content".
   ========================================================================== */

/*
 * 1. THE PROFILE SEGMENTS COULD NOT HOLD THEIR OWN CONTENT AT 320.
 *
 * `repeat(3, minmax(0, 1fr))` lets a track shrink below its intrinsic width -
 * which is what stopped the strip overflowing the page - but the `<a class=btn>`
 * inside neither shrinks nor clips, so the widest segment spilled past its
 * track. Measured: the chef-hat icon painted 5.5 CSS px outside the container
 * and ~12 px outside the active pill, and at the other end a Hebrew letter was
 * drawn inside the neighbouring bookmark glyph.
 *
 * The icons are decorative - each segment's label already names its
 * destination - so they are what gives way on the narrowest phones. Nothing a
 * member reads is lost, and the pill goes back to containing what is in it.
 */
@media (max-width: 430px) {
  .profile-links > .btn { min-width: 0; overflow: hidden; }
  .profile-links > .btn > .rh-icon,
  .profile-links > .btn > svg { display: none; }
}

/*
 * 2. THE PRODUCT'S OWN NAME WAS CLIPPED MID-GLYPH AT 320.
 *
 * `.rh-header__brand` was given `flex: 0 1 auto; overflow: hidden;
 * text-overflow: ellipsis` so it would absorb the header's residual overflow.
 * The graceful part never happened: the element is `display: inline-flex`, and
 * `text-overflow` does not apply to a flex container. So instead of `RecipeH…`
 * the R lost its vertical stem - measured at 93.0 CSS px against 97.5 at 390,
 * a hard vertical cut at a single column with the drop shadow sliced at the
 * same place - and the wordmark read as a corrupt character on every signed-in
 * screen at the narrowest width.
 *
 * A brand mark is not a place to absorb overflow. It never shrinks now, and
 * the space comes from the administrator chip, which drops to its icon on
 * phones - it keeps its accessible name from `aria-label="ניהול"`, so nothing
 * is lost to anyone who cannot see it either.
 */
.rh-header__brand { flex: none; overflow: visible; }

@media (max-width: 430px) {
  .rh-header__admin-label { display: none !important; }
  .rh-header__admin { padding-inline: var(--space-1); }
}


/*
 * THE SCRUBBER IS THE STUDIO, AND AT 320 IT WAS 39px WIDE.
 *
 * The play button, the scrubber and the timecode share one flex row, and the
 * scrubber is the only flexible item - so at 320 it absorbed the whole squeeze:
 * measured 39 CSS px of track with about 21px of usable travel once the thumb
 * is subtracted, against 109px at 390. That is 0.24 seconds per pixel on the
 * five-second demo clip and about 1.4 s/px on a thirty-second one, on the one
 * interaction the whole feature exists for. The 18px thumb is also under
 * WCAG 2.5.8's 24px floor.
 *
 * On a phone the transport becomes two rows: the controls that are a fixed size
 * keep their row, and the scrubber gets a full-width one of its own. Nothing is
 * removed and nothing shrinks below its target size.
 */
@media (max-width: 430px) {
  #react-media-root .row:has(.rh-range) { flex-wrap: wrap; row-gap: var(--space-2); }
  #react-media-root .rh-range {
    order: 2;
    flex: 1 0 100%;
    width: 100%;
    min-width: 0;
  }
  .rh-range::-webkit-slider-thumb { width: 26px; height: 26px; margin-top: -10px; }
  .rh-range::-moz-range-thumb { width: 26px; height: 26px; }
}

/* The scrubber's width, now that no inline style outranks it. */
#react-media-root .rh-range { flex: 1 1 auto; min-width: 0; }

/*
 * THE VALUE COLUMN BROKE ON THE ONE LONG LABEL.
 *
 * `.admin-op__list > div { flex-wrap: wrap }` was added for a different
 * orphaning bug, and with `מתוכם ממתינים להשלמת מידע` it wrapped the FIGURE
 * onto its own line - where the auto margin then put it at the opposite edge
 * from the other three. An operator scanning the column read 50 / 0 / — / 0
 * with a stray 50 floating at the other margin, and the third label looked as
 * though it had no value at all.
 *
 * The stylesheet already states the invariant a few rules up: every row in a
 * card shares one edge and the figures read down. So the ROW does not wrap; the
 * LABEL wraps inside its own cell, which is what has the words in it.
 */
.admin-op__list > div { flex-wrap: nowrap; }
.admin-op__list dt { overflow-wrap: anywhere; text-wrap: pretty; }

/*
 * And the active profile segment had no room left for its own label: the first
 * letter grazed the pill edge at 320. The icons came out at this width, so the
 * padding they were competing with can come back.
 */
@media (max-width: 430px) {
  .profile-links > .btn { padding-inline: var(--space-2); }
}

/* ==========================================================================
   CONTENT VERSUS BOX, CLOSED AS A CLASS. WP-17.5 final software pass.
   ==========================================================================

   This family has now been reported five times by three reviewers, and the
   first four fixes were all per-instance: widen this, unclip that, stop the
   other from shrinking. The defect kept coming back because every fix
   described a symptom and none of them described the rule.

   The rule, and it is measurable:

     usable inner width = border-box width
                        - border-inline-start - border-inline-end
                        - padding-inline-start - padding-inline-end

     A one-line compact control - a segment, a chip, a pill - must contain its
     own rendered text inside THAT width, and must keep a minimum distance
     between the text and any edge it actually draws.

   `QA/content-fit.js` measures it with a DOM Range around the text nodes,
   because every box-based measurement in this repository has been fooled by
   this family at least once: `document.scrollWidth` cannot see it through
   `overflow-x: clip`, the element's own `scrollWidth` cannot see it through
   `overflow: hidden`, and a bounding-box check cannot see it at all - the box
   is exactly where it should be, and the text is not inside it.

   What is deliberately NOT here: a global `min-width: max-content`, a global
   `flex-shrink: 0`, a global `white-space: nowrap`. All three would trade this
   family for the overflow family the previous round spent itself closing, on
   surfaces neither reviewer complained about. Two components were measured
   failing; two components are changed.
   ========================================================================== */

/*
 * 1. THE PROFILE SEGMENT COULD NOT HOLD ITS OWN LABEL AT 320.
 *
 * Measured on the confirmation head, 320px, the selected segment:
 *
 *     container      90.0 px
 *     rendered text  92.7 px          <- wider than the box that contains it
 *     usable inner   74.0 px          (90.0 - 8 - 8)
 *     ink to pill     -1.3 px         <- on BOTH edges: outside, not merely tight
 *
 * `repeat(3, minmax(0, 1fr))` divides the strip into three EQUAL tracks, so
 * every segment is sized by the container and none of them by its own label.
 * `המתכונים שלי` needs 92.7; `שמורים` needs 50.0 and was given the same 90.0,
 * so 40 px of slack sat beside a label that was overflowing. `.btn` carries
 * `white-space: nowrap`, so the label could not give way either, and the
 * `overflow: hidden` added in an earlier round turned the overflow into a
 * clip - the text was cut at both ends AND painted onto the pill's edge.
 *
 * The three labels together need 240.7 px of the 270 px available at 320. They
 * fit. They only failed to fit because the layout refused to let them have
 * their own widths.
 *
 * So the tracks are sized by content and the leftover is shared:
 *
 *   min-inline-size: max-content   no segment is ever narrower than its label
 *                                  plus its padding. Scoped to this component:
 *                                  as a global rule it would be the overflow
 *                                  defect of the previous round, reintroduced.
 *   flex: 1 1 auto                 the slack is distributed, so the strip still
 *                                  fills its width and still reads as one group.
 *   flex-wrap: wrap                the escape hatch. A label longer than the
 *                                  three shipped ones moves to a second row
 *                                  inside a card-shaped container - it is not
 *                                  clipped, not shrunk, and not pushed off the
 *                                  screen. The container already stops being
 *                                  pill-shaped at this width for exactly this
 *                                  reason.
 *
 * Nothing else about the control changes: it is still three `<a>` elements in
 * a `<nav>`, the selected one still carries the surface and the shadow, the
 * focus ring is untouched, `--tap-min` still sets the height, and the strip is
 * still laid out by the document's own direction rather than by a hard-coded
 * side.
 */
@media (max-width: 480px) {
  .profile-links {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
  }
  .profile-links > .btn {
    flex: 1 1 auto;
    /*
     * `min-inline-size` AND NOT `min-width`, and the order matters.
     *
     * In a horizontal writing mode these are the same computed property, so
     * whichever is declared LAST wins. The first version of this rule stated
     * `min-inline-size: max-content` and then `min-width: auto` underneath it
     * to neutralise the `min-width: 0` from the earlier media block - and the
     * second line silently deleted the first. The segments were still sized by
     * their content, but by the flex automatic minimum rather than by the
     * declared floor, so the rule worked and the reason written beside it was
     * not the reason. A product reviewer caught it by reading the computed
     * value instead of the declaration.
     *
     * Declared last, `min-inline-size` outranks the earlier `min-width: 0` on
     * its own and nothing else is needed.
     */
    min-inline-size: max-content;
    /*
     * `min-width: 0` and `overflow: hidden` were the previous round's attempt
     * at this, and together they are what made the failure silent: the box
     * shrank below its content and then hid the evidence, which is why
     * `scrollWidth === clientWidth` reported a clean control while a glyph was
     * being sliced. A segment that cannot be too small does not need to clip.
     */
    overflow: visible;
  }
}

/*
 * 2. THE RECIPE METADATA CHIPS WERE NOT A ROW.
 *
 * Reported as "the chip with the longest label is the narrowest, and its
 * padding collapses to 0.5px". Both halves are true and neither is the cause.
 * Measured at 320 and identical at 390:
 *
 *     60 דקות     box 54.5   text 46.5   padding-inline 0 / 8
 *     בינונית      box 59.7   text 38.7   padding-inline 12 / 8
 *     8 מנות      box 58.0   text 37.0   padding-inline 12 / 8
 *
 * The widths are not decided by content at all - they are 8 px of padding on
 * the first chip against 20 px on the others. And the chips are not one line:
 * the icon sits ABOVE the value, each chip is 48.9 px tall, and the row is
 * three two-line stacks.
 *
 * `.rdetail__fact` is `flex-direction: column` at line ~3583, which is right
 * for the desktop card where the value sits over its note. The phone override
 * re-declared `display: flex` and set `align-items: baseline` and a gap FOR A
 * ROW - and never reset the direction. So the property that would have made it
 * a row was the one property the override did not write, `align-items:
 * baseline` has had no effect on a column since the day it was added, and the
 * comment above it describing "a wrapped run with dividers" has never once
 * described what the browser drew.
 *
 * With the direction stated, the chips size to their content, the icon leads
 * its own value, and the first chip's zero start padding does what it was
 * written to do: align the icon with the page gutter, rather than leave a
 * numeral flush against the edge with no icon in front of it.
 */
@media (max-width: 430px) {
  .rdetail__fact {
    flex-direction: row;
    /* An icon is a box beside a word, not a word: centre it on the line. */
    align-items: center;
    /*
     * Sizing, and the reason the padding could be eaten. `flex: 0 1 auto` with
     * `min-width: 0` lets flexbox take width from an item in proportion to how
     * much it has - so the chip with the LONGEST label is the one that loses
     * the most, which is precisely the complaint. `min-content` lets a chip
     * wrap its own text if it ever has to, and never lets the box be squeezed
     * narrower than the text it is drawing.
     */
    min-inline-size: min-content;
  }

  /*
   * AND THE FIRST CHIP IS A FILLED PILL, NOT A GUTTER-ALIGNED RUN.
   *
   * `:first-child { padding-inline-start: 0 }` was written to align the row
   * with the page gutter, on the understanding that these are "a wrapped run
   * with dividers" - the phone override strips `border` to say so. It strips
   * the border and NOT the `background: var(--color-surface)` and
   * `border-radius: var(--radius-sm)` the chip inherits, so what is actually
   * painted is a white pill with a 10px corner and an invisible divider.
   *
   * On a pill, zero padding on an edge is not alignment; it is the content
   * sitting on the fill's rounded corner - and here it was the clock icon of
   * `60 דקות`, at 0.0px, on the same chip the finding was written about. The
   * same omission as the `flex-direction`, one declaration further on: an
   * override that changed what the component IS and left one property saying
   * what it used to be.
   *
   * The pill keeps its fill and gets the padding a fill requires. The row now
   * begins one --space-2 inside the gutter, which is where a filled chip's
   * TEXT began anyway.
   */
  .rdetail__fact:first-child { padding-inline-start: var(--space-2); }
}

/*
 * 3. THE AUDIT FEED BROKE A FOUR-LETTER HEBREW WORD INTO A COLUMN OF LETTERS.
 *
 * Found by the mobile/RTL reviewer on the head that fixed the other two, and it
 * is the same defect with a different ending: a one-line label given a box it
 * cannot hold. Where the profile segment overflowed its box and the chip stood
 * up in it, this one broke apart inside it.
 *
 * Measured on /admin at 320, the row `שינוי שם מונח`:
 *
 *     action  94.1 px   id  46.9 px   time  54.7 px   target  14.3 px
 *     `מונח`  4 characters, box 14.3 px wide and 69.8 px tall, THREE line boxes
 *
 * Rendered: `מו` over `נ` over `ח`. Identical at 360, two line boxes at 375,
 * clean from 390 up - so it lands on the two most common phone widths shipping.
 *
 * Three declarations, each individually defensible, and the failure is what
 * they do together:
 *
 *   flex: 1              resolves to `1 1 0%`. A basis of zero means the item
 *                        contributes NOTHING to the line-breaking decision, so
 *                        the row's own `flex-wrap: wrap` never engages and this
 *                        item alone absorbs the entire squeeze.
 *   min-width: 0         removes the floor that would have stopped it.
 *   overflow-wrap: anywhere   turns "too narrow" into "broken between letters"
 *                        rather than into visible overflow somebody would fix.
 *
 * `anywhere` and `direction: ltr` were written for a 24-character ObjectId.
 * That id is not in this element - it is in `.admin-recent__id`, which has its
 * own rule and keeps both properties. What is in THIS element is one of six
 * Hebrew nouns from a fixed map in `admin.js` (`משתמש`, `מתכון`, `תגובה`,
 * `דיווח`, `מונח`, `תצורה`), so it is Hebrew that reads right-to-left and has
 * no reason to be broken mid-word.
 *
 * `min-content` is the floor: the label may still wrap BETWEEN words if it ever
 * has two, and can never be narrower than the longest word it is drawing.
 * `1 1 auto` lets the row wrap the way its own `flex-wrap` already says it
 * should. `unicode-bidi: isolate` stays, so the unmapped-type fallback - which
 * would print a Latin `taxonomy_term` - still renders left-to-right inside its
 * own run.
 */
.admin-recent__target {
  flex: 1 1 auto;
  /* Declared last, so it outranks the `min-width: 0` above. See the note on
     `.profile-links > .btn`: a `min-width` written under this line would be the
     same property, and would delete it. */
  min-inline-size: min-content;
  overflow-wrap: normal;
  direction: rtl;
}

/*
 * 4. A THREE-LETTER LABEL DRAWN AS THREE LINES OF ONE LETTER.
 *
 * The audit-feed fix above, one card away on the same page, found by the
 * product reviewer on the head that shipped it. Measured on /admin, the AI row
 * of `מצב תפעולי`:
 *
 *     `מצב`  3 characters, box 6.2 px wide and 69.8 px tall, THREE line boxes
 *
 * Identical at 1100, 1280, 1440, 1600 and 1920, and clean below 1100 - it
 * appears when the ops grid reaches four columns, which is to say it is a
 * DESKTOP defect. This family has now been found at 320 and at 1920, which is
 * the argument against thinking of it as a narrow-screen problem at all.
 *
 * The squeeze arrives from the sibling this time. `dd { flex: none }` so the
 * value `מצב הדגמה — ללא ספק חיצוני` (198.8 px) cannot give way, the row is
 * `flex-wrap: nowrap` so it cannot break, and `dt { flex: 1 1 auto;
 * min-width: 0; overflow-wrap: anywhere }` so the label absorbs all of it and
 * then breaks between its letters.
 *
 * Both of those neighbours were written deliberately and both stay. The ROW
 * still does not wrap and every figure still shares one edge - that was the
 * fix for a figure orphaned at the opposite margin, and it is still right.
 * What changes is WHICH cell gives way:
 *
 *   the label is one short word from a fixed vocabulary and cannot be broken,
 *   so it gets the min-content floor and stops breaking between letters;
 *   the value is a phrase with spaces in it, so it may wrap between words -
 *   which is what a phrase does, and what a three-letter noun cannot.
 *
 * Number values are unaffected: `min-content` on `50` is `50`.
 *
 * ---------------------------------------------------------------------------
 * `anywhere` VERSUS `break-word`, WHICH IS THE WHOLE REASON THIS ROW BROKE
 * ---------------------------------------------------------------------------
 *
 * The first attempt at this rule gave both cells `min-inline-size: min-content`
 * and left `overflow-wrap: anywhere` in place, and it moved the defect rather
 * than fixing it: the value `80` came back as `8` over `0`, in a box 17.1px
 * wide.
 *
 * That is not a flexbox subtlety, it is what the two keywords mean.
 * `overflow-wrap: anywhere` creates a soft break opportunity between EVERY pair
 * of characters AND those opportunities count when the browser computes
 * min-content - so `min-content` under `anywhere` is the width of one
 * character, and a floor of `min-content` is a floor of nothing at all.
 * `break-word` creates the same last-resort break and is explicitly EXCLUDED
 * from min-content sizing, so the floor stays the longest word.
 *
 * So the cells keep a last-resort break for a token longer than the card, and
 * `min-content` becomes the floor it was written to be. `anywhere` was never
 * the keyword this wanted; it was reaching for a fallback and getting a licence.
 */
.admin-op__list dt,
.admin-op__list dd {
  min-inline-size: min-content;
  overflow-wrap: break-word;
}
.admin-op__list dd { flex: 0 1 auto; }

/*
 * The same keyword, in the same trap, one component away. `.rdetail__fact` was
 * given `min-inline-size: min-content` above, and its value span carries
 * `overflow-wrap: anywhere` from the desktop card - so on a phone that floor
 * was one character wide and the comment above it was describing something the
 * browser would not have done. It has never been under enough pressure to show
 * it, because the fact row wraps before a chip has to shrink. A floor that only
 * holds while nothing is leaning on it is worth correcting while it is quiet.
 */
@media (max-width: 430px) {
  .rdetail__fact .rdetail__fact-value { overflow-wrap: break-word; }
}

/*
 * 5. THE ACCOUNT-SETTINGS CARD HUNG 19px OUT OF THE PANEL THAT CONTAINS IT.
 *
 * Reported by both reviewers on the same head, and it is a box-versus-box
 * defect rather than a content-versus-box one - which is why none of the four
 * content rules above can see it, and why it is recorded here rather than
 * registered as a component in the gate.
 *
 * `.form` is `width: 100%`, and `.profile-private__panel > .form` adds
 * `margin-inline: var(--space-5)`. Width plus both margins over-constrains the
 * equation, so the browser drops one margin - in a right-to-left document, the
 * inline-end one - and pins the card to the inline-start edge.
 *
 *     320    form 1 → 279     panel 20 → 300     note 41 → 279
 *     1280   form 261 → 979   panel 280 → 1000   note 301 → 979
 *
 * 19 px out at every width, with the panel's own border ending behind it, and
 * at 320 the card's edge one pixel from the screen. The sibling `<section>`
 * takes the same margin and lays out correctly, because it never asked for
 * 100% of a width it was also being inset from.
 *
 * `width: auto` is the whole fix: a block box already fills its container, and
 * saying so a second time is what broke it.
 *
 * C-7 moved the selector one level down with the markup - the form's parent is
 * the panel now, not the disclosure - and the assertion in `contentFit.test.js`
 * moved with it. The rule is the same rule; the descendant it names is not.
 */
.profile-private__panel > .form { width: auto; }

/*
 * 6. AND THE ADMIN CONTROLS WERE 40px TALL, WHICH ONLY MATTERED ONCE THEY
 *    COULD BE TOUCHED.
 *
 * Found by the responsive matrix on the head that fixed the panels: 4 of 72
 * mobile rows, `טופל` 62x40, `דחה` 66x40, `הסרה` 100x40, on /admin at 320,
 * 360, 390 and 430, against the 44px floor `tokens.css` declares.
 *
 * `components.css` line 677 sets `[data-surface="admin"] .btn { min-height:
 * 40px }`, and that is a deliberate density for a dense management surface -
 * it is not a mistake and it is not reverted here. What changed is that the
 * surface became reachable. Every one of those buttons sat inside a panel that
 * was `display: none` below 720px, so no sweep had ever measured them and no
 * finger had ever been able to miss them. Making the panels visible is what
 * turned a desktop density into a touch target.
 *
 * So the density stays where there is a pointer, and the floor applies where
 * there is a finger. 719px rather than 430px because that is the breakpoint
 * this surface was hidden below - the widths that just gained the admin area
 * are exactly the widths that need its controls to be tappable.
 *
 * `--tap-min` (48px), RAISED FROM 44 ON 2026-08-25. The design package's
 * ruling - "יומן החלטות - סבב הצפות 1", marked binding - sets 48 for every
 * standalone control and reserves 44 for interactive chips alone. An admin
 * action button is a control, so it takes the control floor.
 *
 * The pointer density above is UNCHANGED at 40px and must stay that way: a
 * global search-and-replace briefly raised it too, which would have thinned a
 * dense management surface on the one input where density is the point.
 */
@media (max-width: 719px) {
  [data-surface="admin"] .btn { min-height: var(--tap-min); }
}

/*
 * 7. AND ON THE ONE ADMIN PANEL THAT NESTS ITS ACTIONS, THEY WERE OFF-SCREEN.
 *
 * Found by BOTH reviewers, independently, on the head that made the panels
 * visible. It is the third defect in this session that was drawn all along and
 * could not be measured because the surface around it was `display: none`.
 *
 * `/admin` -> `משתמשים`, at 320:
 *
 *     #admin-user-list   clientWidth 278   scrollWidth 420   overflow: hidden
 *     מחיקה              left -96 -> right -24.3, on every one of 11 rows
 *     השעיה              sliced, 24-83% visible
 *
 * And it cannot be scrolled to. `.admin-list` is `overflow: hidden`, not
 * `auto`, so a pan leaves `scrollLeft` at 0; `html`/`body` are `overflow-x:
 * clip`, so the document never grows either. A keyboard user reaches the button
 * because `.focus()` may set `scrollLeft` on a clipped box. A finger cannot.
 * Both reviewers noted that a headless `click()` reports success for the same
 * reason - actionability calls `scrollIntoViewIfNeeded`, which is not a gesture.
 *
 * WHY ONLY THIS PANEL. Every other admin row puts its action `.row` as a DIRECT
 * child of the article, so the article's own `flex-wrap: wrap` drops the actions
 * to a second line. `userRow()` wraps both halves in ONE `<div class="row
 * u-between">`, so the article has a single child and its `flex-wrap` has
 * nothing to wrap. That single child then matches
 * `.admin-list .row { flex: 0 0 auto }` - correct for a group of buttons that
 * should not stretch, and wrong for the element that IS the row - so it keeps
 * its 323.6px max-content width inside a 278px card.
 *
 * A `.row` that is an admin article's only child is not a group inside the row;
 * it is the row. It takes the full width and shrinks, and `.row` already wraps
 * by default, so the actions drop to their own line exactly as they do in every
 * other panel. Nothing is restyled and no markup changes - `flex: 0 0 auto`
 * still governs every action group, which is what it was written for.
 */
.admin-list > article > .row:only-child,
.admin-list > .card > .row:only-child {
  flex: 1 1 100%;
  min-inline-size: 0;
}

/*
 * 8. THE IMPORTED SOURCE URL, AGAIN - AND THE ELLIPSIS THAT NEVER RAN.
 *
 * Found by the mobile/RTL reviewer on the head that fixed the admin rows.
 * `/recipes/:id/edit` on an imported recipe, measured at 320:
 *
 *     body.scrollWidth 906   body.clientWidth 320   ->  586px of overflow
 *     documentElement.scrollWidth 320                ->  every gate reads ZERO
 *     code.media-source-url   844.5px wide, x = -585.5 .. 259
 *     ellipsisEngaged = FALSE
 *
 * The `✕` that removes the source sat at x = -163: off the screen, with no
 * gesture that reaches it, because `html`/`body` are `overflow-x: clip`.
 *
 * A rule ~3,500 lines up already fixed this element once, as a CLASS, and the
 * comment there is worth re-reading: "the next long URL will not be in
 * `media-source-url`". It gave the group `min-width: 0`, `overflow-wrap:
 * anywhere` and `word-break: break-word`. All three are still there. All three
 * are inert here, and for two separate reasons:
 *
 *   1. `white-space: nowrap` (line ~532) means there are no line-break
 *      opportunities at all, so `overflow-wrap` and `word-break` have nothing
 *      to act on and the element's min-content width is the entire URL. That
 *      nowrap is DELIBERATE - the row is meant to be "one line, elided" - and
 *      it stays.
 *   2. `min-width: 0` lets a flex item shrink along the MAIN axis. Below 719px
 *      the toolbar is `flex-direction: column`, so width is the CROSS axis,
 *      `align-items: stretch` sizes every child to the line's cross size, and
 *      the largest hypothetical child sets it. Shrinking never entered into it.
 *      Measured: all three children exactly 844.5px inside a 198px toolbar.
 *
 * So the element grew instead of its text being clamped, and `overflow: hidden;
 * text-overflow: ellipsis` - which is right there on the same rule - never had
 * anything to elide. The graceful behaviour was written, and the box outran it.
 *
 * `max-inline-size: 100%` is the missing half of that earlier class fix: an
 * element that holds a machine-generated address may not be wider than the
 * thing that contains it, whichever axis the container happens to be using.
 * With it the URL is clamped to the toolbar, the ellipsis engages, and the
 * remove control comes back on screen.
 *
 *     after, 320:  body overflow 0   url 198px   ellipsisEngaged TRUE
 */
.media-source-url,
.import-demo code,
.composer__preview,
.field__hint,
.field__error {
  max-inline-size: 100%;
}

/* ==========================================================================
   PUBLISH READINESS. FIND-014, the imported-draft dead end.
   ==========================================================================

   The human WORK review found an owner unable to tell whether their own draft
   could be published. The product's whole answer was a badge saying `טיוטה` and
   a line saying only they could see it - true, and useless. Every publish
   attempt returned the same sentence, which says THAT it failed and never WHAT
   to do.

   This is a task list with one action at the end of it, so it is styled as a
   panel a member reads once and acts on, not as an alert they dismiss. The
   three states differ only in their accent, because they are the same object at
   three distances from done - a state that changed layout would make "almost
   ready" look like a different feature from "needs information".
   ========================================================================== */

.readiness {
  margin: 0 0 var(--space-5);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-border);
  border-inline-start: 3px solid var(--color-accent);
  border-radius: var(--radius-card);
  background: var(--color-surface);
}

/*
 * THE BOX THE READINESS PANEL WILL FILL, RESERVED WHILE ITS OWN REQUEST IS IN
 * FLIGHT. `CP43-02`.
 *
 * The panel comes from a SECOND fetch - deliberately, because readiness is the
 * server's answer and a view model deciding it is how a product tells somebody
 * they may publish and then refuses them (`docs/49` FD-002) - and it renders
 * ABOVE the hero it describes. So it arrived about 100ms after the recipe and
 * pushed the entire page down: `QA/product-embed-performance.js` measured
 * **CLS 0.308** on a draft against **0.019** on a published recipe, and the
 * shift named `header.rdetail__hero`.
 *
 * `16.5rem` is the panel's own measured height - 263px at 390 and 271px at 1280
 * across this catalogue's drafts, 302px for the one with the longest list - so
 * the reservation is the ordinary case and the residual is one panel's extra
 * line rather than a whole panel.
 *
 * It carries no border and no background on purpose. An empty bordered card for
 * a tenth of a second reads as a broken panel; empty space reads as a page that
 * has not finished, which is what it is.
 */
.readiness--pending {
  margin: 0 0 var(--space-5);
  min-height: 16.5rem;
}

.readiness--ready { border-inline-start-color: var(--rh-olive-600, var(--color-success, #4d7c0f)); }
.readiness--blocked { border-inline-start-color: var(--color-danger, #b91c1c); }

.readiness__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font: var(--type-title-sm, var(--type-label));
  color: var(--color-text-strong);
}

.readiness__lead {
  margin: var(--space-2) 0 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

/*
 * A list of what is missing, not a form. Each row names the thing and then the
 * action, because "תמונת שער" alone tells somebody what is absent and not what
 * to do about it.
 */
.readiness__list {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.readiness__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  /* The rows are short phrases; a divider between them reads as a table. */
  padding-block: var(--space-1);
}

.readiness__what {
  font: var(--type-label);
  color: var(--color-text-strong);
}

.readiness__todo {
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

/*
 * THE COST NOTICE.
 *
 * SEPARATED BY A RULE, and the rule is the whole design. Everything above it
 * is a list of things that BLOCK publication; this blocks nothing. An author
 * who reads it as a seventh missing field will go looking for a way to fix
 * `מלח לפי הטעם`, and there is nothing to fix - a recipe is allowed to have an
 * ingredient nobody can price.
 *
 * So it is quieter than the list above it, not louder: muted text, no warning
 * colour, no icon that means danger. `data-cost-notice` carries `partial` or
 * `none` for anything that needs to tell them apart, and neither state paints
 * red. The strongest thing this component says is that a number will be
 * missing.
 */
.readiness__cost {
  margin-block-start: var(--space-4);
  padding-block-start: var(--space-3);
  border-block-start: 1px solid var(--color-border-subtle, var(--color-border));
}

.readiness__cost-lead {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font: var(--type-body-sm);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

.readiness__cost-list {
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/*
 * The name and its reason on one line where there is room and two where there
 * is not. `flex-wrap` rather than a media query: the panel appears both in the
 * editor's narrow column and on the recipe page, and the width that matters is
 * the container's rather than the viewport's.
 */
.readiness__cost-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
}

.readiness__cost-name {
  font: var(--type-label);
  color: var(--color-text-strong);
}

.readiness__cost-reason {
  font: var(--type-body-sm);
  color: var(--color-text-muted);
}

/*
 * NO `min-inline-size: min-content` HERE, and that is a deliberate omission.
 *
 * The first draft of this panel pinned both rows to their intrinsic width out
 * of habit, carried over from the content-versus-box family the previous work
 * package closed. `tests/contentFit.test.js` rejected it, correctly: that rule
 * says a component may pin its own intrinsic width only after
 * `QA/content-fit.js` has measured it failing without the pin.
 *
 * Neither had been measured. Both are flex items in a wrapping row with no
 * `overflow: hidden` and no `min-width: 0`, so the flex automatic minimum
 * already holds them at min-content - the pins were restating a default while
 * looking like evidence of a fixed defect. If a measurement ever shows they
 * are needed, they come back WITH the measurement attached.
 */

/* ONE dominant next action. Section 21: never a draft with nothing to press. */
.readiness__action { margin-top: var(--space-4); }

@media (max-width: 430px) {
  .readiness { padding: var(--space-3) var(--space-4); }
  /* Full width, because it is the only action on the panel and a phone has no
     room for it to share a line with anything. */
  .readiness__action { width: 100%; }
}

/* ==========================================================================
   SKELETONS AND EMPTY STATES. FIND-002 and FIND-008, Stage C-9 / C-10.
   ==========================================================================

   FIND-002: the feed appeared blank for several seconds. It was neither broken
   nor silent - the status region already announced `טוען…` to assistive
   technology - but a sighted member met an empty column and could not tell a
   slow request from an empty feed.

   FIND-008: the empty states described absence accurately and then left the
   member on a blank screen. The copy was never the problem; there was nowhere
   to go.

   No progress percentage and no spinner that implies one. §43 forbids faking
   progress, and a skeleton is honest in a way a 40% bar is not: it says "cards
   are coming and this is their shape", which is exactly what is known.
   ========================================================================== */

.rcard--skeleton {
  /* The same box as a real card, so nothing jumps when content replaces it. */
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  pointer-events: none;
}

.rcard__skeleton-media {
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
}

.rcard__skeleton-lines { display: grid; gap: var(--space-2); }

.rcard__skeleton-line {
  height: 0.75rem;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}
.rcard__skeleton-line--title { height: 1.1rem; inline-size: 65%; }
.rcard__skeleton-line--short { inline-size: 40%; }

/*
 * A slow, low-contrast pulse. Fast or high-contrast reads as an alert, and this
 * is the opposite of an alert - it is the screen saying "nothing is wrong".
 */
@keyframes rh-skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.rcard__skeleton-media,
.rcard__skeleton-line {
  animation: rh-skeleton-pulse 1.6s var(--ease-out, ease-in-out) infinite;
}

/*
 * Somebody who asked for less motion gets a static placeholder. The skeleton
 * still communicates shape without moving, so nothing is lost by honouring it.
 */
@media (prefers-reduced-motion: reduce) {
  .rcard__skeleton-media,
  .rcard__skeleton-line { animation: none; }
}

/*
 * The empty state, now that it carries an action. Centred and narrow, because
 * it is a sentence and a door - not a layout.
 */
.feed__empty {
  display: grid;
  justify-items: center;
  gap: var(--space-4);
  max-width: 42ch;
  margin-inline: auto;
  padding: var(--space-7) var(--space-4);
  text-align: center;
  color: var(--color-text-muted);
  text-wrap: pretty;
}

.feed__empty-text { font: var(--type-body-sm); }

/* ==========================================================================
   C-SOCIAL-1 + C-SOCIAL-2 — THE IMMERSIVE FEED. docs/49 FD-005, docs/53.
   ==========================================================================

   Product decided the discovery surface is media-first. The rule is about
   CONTEXT, not theme: discovering means media dominates. Cooking, creating and
   administering are untouched and keep Warm Table - Recipe Detail, Cook Mode,
   the editor, Admin, advanced search and the course surfaces are explicitly out
   of scope.

   Both increments land together, and docs/53 records why: the card carries four
   facts, a source chip, a match reason and a safety line at comparable weight,
   so making it `100dvh` WITHOUT re-ranking would produce a full-screen metadata
   card - measurably worse than the current feed and the opposite of the
   direction.

   MECHANICS ARE FIXED, not a preference. Native CSS snap only: no wheel
   interception, no touchmove hijack, no paging library, and the document is
   never the pager. `dvh` rather than `vh`, because a mobile toolbar that
   collapses mid-scroll changes `vh` and would resize every item under the
   reader's finger.

   The pagination button stays. It is the existing lifecycle, it is reachable by
   keyboard, and swapping it for scroll-triggered loading is a separate decision
   with its own failure modes that no finding asked for.
   ========================================================================== */

@media (max-width: 719px) {
  /*
   * THE FEED FILLS WHAT IS LEFT OF THE VIEWPORT, MEASURED RATHER THAN GUESSED.
   *
   * The first version of this block sized the scroller as
   * `100dvh - tabbar - safe-area`, verified that one card was exactly as tall
   * as the scroller, and shipped. Both facts were true and the result was
   * wrong: the scroller does not START at the top of the viewport. The
   * application header is 68px and the feed's own header is another 96, so a
   * 780px list began at y=164 and ended at y=944 on an 844px screen - the
   * bottom 100px of every card, which is exactly where the action rail lives,
   * sat below the fold at rest. C-SOCIAL-3 is what made it visible, because
   * `Open Recipe` is the first control anybody looks for.
   *
   * The lesson is the one the responsive harness keeps teaching: A HEIGHT IS
   * NOT A POSITION. So nothing here is a viewport arithmetic any more. The
   * shell is pinned to one viewport, every wrapper between it and the list
   * passes that height down, and the list takes what is left - which cannot be
   * wrong when a header changes size, when a notice appears, or on a device
   * whose chrome nobody measured.
   */
  body:has(.page-feed) {
    block-size: 100dvh;
    /* The document is not the pager. Without this the page itself scrolls, and
       a feed with two scrollers has no defined resting position at all. */
    overflow: hidden;
  }

  body:has(.page-feed) > main,
  body:has(.page-feed) > main > .container,
  body:has(.page-feed) .feed-layout,
  .page-feed {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    /* Flex items refuse to shrink below their content without this, which is
       how one long card would push the whole column past the viewport again. */
    min-block-size: 0;
  }

  /* Full bleed. Side gutters are the last of the card grammar on a surface
     whose entire argument is the photograph; the meta block keeps its own
     padding, and the chrome around the list gets it back explicitly. */
  body:has(.page-feed) > main > .container { padding-inline: 0; }

  /*
   * The 158px this surface was spending on nothing.
   *
   * `main` carries 1rem of top padding and 4.5rem of bottom clearance for the
   * tab bar, and `body:has(.rh-tabbar)` reserves that bar's height a second
   * time - harmless on a scrolling page, and 88px of food on a page pinned to
   * one viewport. The reserve stays on the shell, where the bar actually is.
   *
   * The site footer is a tagline. On a surface that is one photograph at a
   * time, under a tab bar that carries the navigation, it is 70px of furniture
   * below the fold on every other phone screen anyway - and here it was pushing
   * the action rail off the card.
   */
  body:has(.page-feed) > main { padding-block: 0; }
  body:has(.page-feed) > .site-footer { display: none; }
  .page-feed .feed__header,
  .page-feed .feed__notice,
  .page-feed .feed__more { padding-inline: var(--space-4); }

  /*
   * The page title is redundant on the immersive surface and costs 40px of
   * food: the tab bar below already names where the reader is, and the two
   * tabs name what they are looking at. It stays in the document for the
   * heading outline and for assistive technology - hidden, not deleted.
   */
  .page-feed .feed__title {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  /*
   * A dedicated internal scroller. The feed owns its own scrolling so the
   * document does not page it, which is what lets one item rest per gesture
   * without any script running on scroll.
   */
  .page-feed .feed__list {
    flex: 1 1 auto;
    min-block-size: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scroll-snap-type: y mandatory;
    /* Gapless: the gap belongs inside the item, or the snap point sits in dead
       space between cards and a rest position looks like a mistake. */
    gap: 0;
    padding-block: 0;
    scrollbar-width: none;
  }
  .page-feed .feed__list::-webkit-scrollbar { display: none; }

  .page-feed .feed__list > .rcard {
    scroll-snap-align: start;
    /* MANDATORY for one-item-at-a-time: without it a fast flick crosses several
       snap points and the reader loses their place in the food. */
    scroll-snap-stop: always;
    /* One item owns the scroller, whatever the scroller turned out to be.
       `min-block-size` and not `block-size`, so a card whose meta block is
       unusually tall grows instead of clipping its own action rail. */
    min-block-size: 100%;
    display: grid;
    /* Media takes the room that is left; the meta block is only as tall as its
       content. This is the whole hierarchy in one line. */
    grid-template-rows: 1fr auto;
    /* Card soup removed on this surface only. Depth and space carry the
       hierarchy instead of an outline around every region. */
    border: 0;
    border-radius: 0;
    box-shadow: none;
    /*
     * THE LEGACY FEED CARD IS A MEDIA SURFACE, NOT AN APPLICATION ONE.
     *
     * This block styles `.rcard` inside `.page-feed` - the shape the Feed had
     * before the immersive shell, which no longer renders here. It read
     * `--fl-feed-bg`, the token that used to paint the whole application black
     * and has been deleted precisely so nothing can do that again.
     *
     * Kept rather than removed, because the class is still on the section and a
     * fallback render would otherwise be unstyled - but pointed at the media
     * ground, which is what a full-bleed card behind a photograph actually is.
     */
    background: var(--fl-media-ground);
    color: var(--fl-media-on);
  }
  .page-feed .feed__list > .rcard:hover { box-shadow: none; }

  /*
   * FOOD FIRST. The media fills the stage it is given rather than a fixed 4:3
   * block sitting above a pile of text.
   */
  .page-feed .rcard__media {
    aspect-ratio: auto;
    block-size: 100%;
    min-block-size: 0;
    background: var(--fl-media-ground);
  }
  /*
   * SOURCE-AWARE FIT, the resolved research conflict. `contain` is the safe
   * default because a landscape cooking scene cropped to a vertical stage loses
   * the cooking. Portrait media is upgraded to `cover` by the client, which is
   * the only place the real dimensions are known.
   */
  .page-feed .rcard__media img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: contain;
  }
  .page-feed .rcard--portrait .rcard__media img { object-fit: cover; }

  /*
   * The meta block reads over the food, so it needs its own ground - and it is
   * measured, because on this surface every row it spends is a row the
   * photograph does not get. Tighter gaps and one padding step less, with
   * nothing removed: the facts, the provenance, the match reason and the
   * explanations all still render, at the weight C-SOCIAL-2 gave them.
   */
  .page-feed .rcard__body {
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    background: var(--fl-feed-surface);
  }

  /*
   * `.rcard__title a` AND NOT ONLY `.rcard__title`, and this is the whole shape
   * of what J-2 found. The heading was recoloured for the dark surface; the text
   * a person reads lives in the LINK inside it, and `.rcard__title a` carries
   * its own `--color-text-strong`, which is more specific about colour than a
   * rule on the parent. So the recipe title — the PRIMARY field on a discovery
   * card (docs/53 §17) — rendered `#241d18` on `#1d1917`: a contrast ratio of
   * 1.05, which is not dim, it is invisible.
   *
   * Nothing in this repository could see it. The three sweeps and
   * QA/j1-acceptance.js all measure boxes, and that title's box is correct, in
   * the right place, with text that fits it. CONTRAST IS NOT A BOX.
   * QA/j2-contrast.js is the gate that can, and it is the newest member of
   * standing constraint 13's family: illegible is not out of bounds.
   */
  .page-feed .rcard__title,
  .page-feed .rcard__title a {
    font: var(--type-title-sm, var(--type-label));
    color: var(--fl-feed-on-media);
  }
  .page-feed .rcard__creator,
  .page-feed .rcard__byline { color: var(--fl-feed-on-media); }

  /*
   * SECONDARY, and it has to LOOK secondary. Four facts, the source chip and
   * the match reason stay truthful and present - the finding was never that
   * they are wrong, it is that they shout at the same volume as the food.
   *
   * THE DARK-SURFACE COLOUR GOES ONLY ON TEXT PAINTED ON THE DARK SURFACE, and
   * the first version of this rule is the other half of the same defect: it
   * also recoloured `.rcard__source`, `.rcard__match` and `.rcard__why`, which
   * keep their own LIGHT pill backgrounds on this surface. A grey chosen to be
   * legible on `#1d1917` landed on cream and measured 2.00. One selector too
   * narrow on the title and three too wide here, in one block.
   *
   * `.rcard__fact` and `.rcard__count` are named explicitly because each has its
   * own colour rule further up the file, and a rule on the CONTAINER does not
   * beat a rule on the element (1.17 and 3.57 respectively).
   */
  .page-feed .rcard__meta,
  .page-feed .rcard__source,
  .page-feed .rcard__match,
  .page-feed .rcard__why {
    font-size: var(--text-2xs);
  }
  /*
   * COLOUR ONLY, AND EVERY font-size ABOVE IS EXACTLY WHAT IT WAS. The obvious
   * shorter fix — adding `.rcard__fact` to the block above — also shrinks the
   * facts from 15px to 12px, because `.rcard__fact` sets its own `font`, and a
   * size change moves boxes and re-opens `content-fit` and the 280-row matrix
   * for every feed row. This increment is paint, so the geometry evidence at
   * `6ee6b9b` still describes the rendered product.
   */
  .page-feed .rcard__meta,
  .page-feed .rcard__fact,
  .page-feed .rcard__count { color: var(--fl-feed-muted); }

  /*
   * THE ALLERGEN LINE IS NOT SECONDARY, and this rule exists to say so out
   * loud. docs/49 FD-005: it may be re-ranked but never hidden, abbreviated to
   * an icon, or put behind an interaction. Every recipe in the catalogue is
   * unreviewed, so it appears on every card - which is exactly why a
   * media-first pass would be tempted to lose it.
   *
   * `--color-warning` and not `--color-warning-text` ON THIS SURFACE ONLY.
   * amber-800 is the accessible warning colour on paper; on `#1d1917` it is
   * 2.19, so the line that must never be demoted was the second-least readable
   * thing on the card. amber-500 is the same warning hue at 7.6 here. Standing
   * constraint 6 - safety is never demoted for weight - and invisibility is the
   * most complete demotion available.
   */
  .page-feed .rcard__safety {
    font-size: var(--text-xs);
    color: var(--color-warning);
  }
}

/*
 * DESKTOP: centre the media rather than stretching a card to dashboard width.
 * The Reels lesson is that caption length must not move the media, so the meta
 * column is bounded and the media keeps its own geometry.
 */
@media (min-width: 720px) {
  .page-feed .feed__list > .rcard { max-inline-size: 42rem; margin-inline: auto; }
}

/* ==========================================================================
   C-SOCIAL-3 — CREATOR IDENTITY AND THE ACTION RAIL. docs/49 FD-005, docs/53.
   ==========================================================================

   The brief fixes the order on a discovery surface and this is the half of it
   that is not the photograph: FOOD FIRST, CREATOR SECOND, SOCIAL ACTION THIRD,
   and a clear way into the recipe. Two controls are new - Follow, and an Open
   Recipe action that says what it is in words - and both are drawn only when
   they are TRUE: Follow needs the server's answer about this viewer, and the
   card renders nothing rather than guessing.

   These rules are not feed-scoped. `.rcard` is produced in one place, so they
   describe the card wherever it appears; the dark immersive treatment is the
   block at the end and overrides paint only.
   ========================================================================== */

.rcard__creator-name {
  /* A display name is arbitrary length and the card is not. One line, and the
     rest is on the profile the name links to.

     `min-inline-size: 0` on this element AND on both flex ancestors, because
     ellipsis needs a shrinkable chain: a `nowrap` span inside a flex item that
     may not shrink below its content sets a min-content width for the whole
     card, and at 320 that width was 448px. */
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rcard__creator { min-inline-size: 0; }
.rcard__byline { min-inline-size: 0; }

/*
 * THE RAIL WRAPS. It gained a labelled control and stopped fitting 320.
 *
 * Six actions and one of them carrying words is 448px of min-content, and the
 * alternative to wrapping is one of the two failures this project refuses: an
 * action pushed off the inline edge, or a label shortened until it stops saying
 * what it does. A second row on the narrowest phone costs 44px and keeps every
 * action reachable with its meaning intact.
 */
.rcard__actions { flex-wrap: wrap; }

/* Pushed to the end of the row, so a long display name shortens instead of
   pushing the control off the card. */
.rcard__follow {
  appearance: none;
  margin-inline-start: auto;
  /* The floor, not a smaller pill that happens to look right. The responsive
     sweep measures every control on a phone and 36px is a finding. */
  min-block-size: var(--tap-min);
  padding-inline: var(--space-4);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--color-text);
  font: var(--type-label);
  white-space: nowrap;
  cursor: pointer;
}

/*
 * Followed is a STATE, painted from the attribute that carries it - the same
 * arrangement the recipe page and the profile use. `social.js` repaints
 * `aria-pressed`, so the control cannot look followed while announcing the
 * opposite.
 */
.rcard__follow[aria-pressed="true"] {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

/* Provenance moved out of the creator row onto its own line: the member who
   posted and the source that inspired it are two different people, and one row
   carrying both is the confusion the credit exists to prevent. */
.rcard__body > .rcard__source { align-self: flex-start; }

/*
 * OPEN RECIPE. The only action in the rail carrying a label, because a feed
 * exists to send somebody to a recipe and everything else in the rail is a
 * reaction to one.
 */
.rcard__open {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: var(--tap-min);
  margin-inline-end: auto;
  padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font: var(--type-label);
  text-decoration: none;
  white-space: nowrap;
}

.rcard__open:hover { background: var(--color-accent-hover); }
.rcard__open .rh-icon { flex: 0 0 auto; }

/* ==========================================================================
   C-SOCIAL-4 — THE CONTEXTUAL COMMENTS SHEET.
   ==========================================================================

   Opened over the feed rather than navigated to, because the place in a feed is
   the only state a feed has. A bottom sheet on a phone and a centred dialog on
   a wide screen: the same panel, put where the thumb or the pointer already is.
   ========================================================================== */

.fsheet {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.fsheet[hidden] { display: none; }

.fsheet__scrim {
  position: absolute;
  inset: 0;
  background: var(--fl-feed-scrim);
  /* Stops a drag over the scrim chaining into the feed underneath, which would
     move the reader's place while they read a comment. */
  touch-action: none;
}

.fsheet__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  inline-size: min(100%, 34rem);
  /* Never taller than the viewport, and never so short that two comments and
     the composer cannot coexist. */
  max-block-size: min(80dvh, 40rem);
  border-start-start-radius: var(--radius-lg);
  border-start-end-radius: var(--radius-lg);
  background: var(--fl-feed-sheet);
  color: var(--color-text);
  box-shadow: var(--shadow-3);
  animation: fsheet-rise 180ms ease-out;
}

@keyframes fsheet-rise {
  from { transform: translateY(12px); opacity: .7; }
  to { transform: translateY(0); opacity: 1; }
}

.fsheet__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border-block-end: 1px solid var(--fl-feed-line);
}

.fsheet__title {
  flex: 1 1 auto;
  min-inline-size: 0;
  margin: 0;
  font: var(--type-label);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fsheet__close {
  appearance: none;
  display: grid;
  place-items: center;
  inline-size: var(--tap-min);
  block-size: var(--tap-min);
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--fl-feed-action-bg);
  color: var(--color-text);
  cursor: pointer;
}

.fsheet__body {
  flex: 1 1 auto;
  overflow-y: auto;
  /* The sheet is the scroller while it is open; the feed behind it is not. */
  overscroll-behavior: contain;
  padding: var(--space-4);
}

.fsheet__list { display: grid; gap: var(--space-4); }

.fsheet__empty {
  margin: 0;
  color: var(--color-text-muted);
  font: var(--type-body-sm);
}

.fsheet__more { margin-block-start: var(--space-4); }

.fsheet__foot {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
  padding-block-end: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  border-block-start: 1px solid var(--fl-feed-line);
}

.fsheet__row { display: flex; gap: var(--space-2); align-items: flex-end; }

.fsheet__row textarea {
  flex: 1 1 auto;
  min-inline-size: 0;
  min-block-size: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--fl-feed-line);
  border-radius: var(--radius-md);
  background: var(--fl-feed-surface);
  color: var(--color-text);
  font: var(--type-body-sm);
  resize: vertical;
}

.fsheet__all {
  color: var(--color-text-muted);
  font: var(--type-caption);
}

.fcomment { display: grid; gap: var(--space-1); }

.fcomment__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  font: var(--type-caption);
  color: var(--color-text-muted);
}

.fcomment__author { color: var(--color-text-strong); }

.fcomment__body {
  margin: 0;
  font: var(--type-body-sm);
  color: var(--color-text);
  /* Comments are typed prose: the newlines are the writer's, and a word longer
     than the panel is the writer's too - neither may push the sheet sideways. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* C-10 again: something to look at while the first page is fetched, and
   nothing claiming to know how long it will take. */
.fcomment--skeleton { gap: var(--space-2); }

.fcomment__skeleton-line {
  display: block;
  block-size: .8rem;
  border-radius: var(--radius-sm);
  background: var(--fl-feed-action-bg);
  animation: rh-skeleton-pulse 1.4s ease-in-out infinite;
}

.fcomment__skeleton-line--short { inline-size: 45%; }

@media (prefers-reduced-motion: reduce) {
  .fsheet__panel { animation: none; }
  .fcomment__skeleton-line { animation: none; }
}

/* The document does not scroll while a modal sheet is open. The feed list has
   its own scroller and keeps its own position; this stops the page behind the
   scrim from moving on a browser that scrolls the document on focus. */
.has-fsheet, .has-fsheet body { overflow: hidden; }

/*
 * DESKTOP: a centred dialog rather than a bottom sheet. The shape of a sheet is
 * an argument about thumbs; a pointer has no such constraint, and a panel
 * pinned to the bottom edge of a 1440 screen is a long way from the card that
 * opened it.
 */
@media (min-width: 720px) {
  .fsheet { align-items: center; }
  .fsheet__panel {
    border-radius: var(--radius-lg);
    max-block-size: min(70dvh, 44rem);
  }
}

/* ==========================================================================
   The two new controls on the immersive surface.
   ========================================================================== */

@media (max-width: 719px) {
  .page-feed .rcard__creator { color: var(--color-text); }

  .page-feed .rcard__follow {
    border-color: var(--fl-feed-line);
    background: var(--fl-feed-action-bg);
    color: var(--color-text);
  }

  /* Followed reads as DONE on a dark surface rather than as a second call to
     action: the filled state belongs to the thing not yet done. */
  .page-feed .rcard__follow[aria-pressed="true"] {
    background: transparent;
    border-color: var(--fl-feed-line);
    color: var(--color-text-muted);
  }

  /*
   * THE RAIL HOLDS ONE ROW, and this is what it costs.
   *
   * Six controls, one of them carrying words, come to more than 390px of
   * min-content at the default spacing - so the rail wrapped to two rows and
   * the photograph paid 44px for it on every phone. The gap goes to zero and
   * the icon controls to one spacing step of padding: the tap targets keep
   * their 44px height, the label keeps its words, and nothing moves off the
   * card. At 320 it still wraps, which is the correct outcome at 320 - an
   * action pushed off the inline edge is unreachable, and a shortened label
   * stops saying what it does.
   */
  .page-feed .rcard__actions { gap: 0; padding-top: var(--space-2); }
  .page-feed .rcard__actions .rh-rail__btn { padding-inline: var(--space-1); }

  /*
   * Open Recipe takes its own row, deliberately, rather than being the control
   * that happens to wrap.
   *
   * Six controls come to 392px of min-content and a phone offers 358, so
   * SOMETHING wraps. Fitting them on one line would mean shaving the label or
   * the padding until it just fits at 390 and breaks on the next device - the
   * knife-edge kind of layout this project keeps finding in review. A
   * full-width action below the icons is the same 44px, reads as the primary
   * thing to do, and sits closest to the thumb.
   */
  .page-feed .rcard__open {
    flex: 1 0 100%;
    order: 1;
    justify-content: center;
    margin-inline: 0;
    margin-block-start: var(--space-2);
  }

  .page-feed .rcard__actions [data-social-error] { flex: 1 0 100%; order: 2; }
}

/* ==========================================================================
   C-6 — CREATOR DISCOVERY.
   ==========================================================================

   `/users` was a member directory: an avatar, a display name, a handle and a
   bio, twelve to a page. FIND-P2-05 named the cost precisely - in a product
   about food, people were discoverable by row rather than through what they
   cook - and docs/52 recorded the disposition as MOVE, not DELETE. Same route,
   same query, same course-graded List/Search surface, different unit.

   The card reads in the order the master prompt gives every discovery surface:
   FOOD, then CREATOR, then one ACTION. The food is a real strip of that
   person's published recipes, each tile a link to the recipe itself, which is
   what "discovery through food" has to mean if it means anything - you arrive
   at a person by seeing something you want to cook.
   ========================================================================== */

/*
 * THE LIST RESET IS NOT COSMETIC HERE. IT IS THE 320 OVERFLOW.
 *
 * The server-rendered directory was a `<div>`. React renders a real `<ul>` of
 * `<li>`, which is the better markup - a directory of creators IS a list - and
 * it arrived with the user agent's `padding-inline-start: 40px` intact.
 *
 * In RTL that is forty pixels of padding on the RIGHT, so at 320 the grid's
 * content box is 240px wide while its column minimum is 280. The column
 * overflows by exactly forty, and in a right-to-left writing mode it overflows
 * to the LEFT - where `document.scrollWidth` cannot see it. Every one of the
 * twenty-one cards began twenty pixels off the left edge of the screen while
 * `documentOverflow` reported zero, which is why this survived the migration's
 * responsive passes and was found by `QA/c6-verify.js` measuring element boxes
 * instead.
 *
 * The block margin is stated rather than inherited from the user agent for the
 * same reason: a value nobody chose is a value nobody can defend.
 */
.creator-grid {
  display: grid;
  margin: var(--space-4) 0;
  padding: 0;
  list-style: none;
  gap: var(--space-5);
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  /* Cards in a row are as tall as their content, not as tall as the tallest:
     a short bio should not stretch into a field of empty card. */
  align-items: start;
}

.creator-card {
  display: grid;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  /* The food strip runs to the card's own edge, so the corners have to cut it.
     Nothing here is interactive-and-clipped: the tiles are inside the box, not
     spilling out of it, so this is a rounding, not a wall. */
  overflow: hidden;
}

/* -- 1. THE FOOD ---------------------------------------------------------- */

/*
 * A LEAD DISH AND TWO BESIDE IT, in a square.
 *
 * The first attempt was three equal tiles in a row, and the headed run measured
 * why that is not food-led: three tiles across a 300px card are 100px each, so
 * the strip came to 139-165px against a 227px block of words. The card said it
 * was about food and the geometry said it was about a bio.
 *
 * The square is the fix and it is deliberate. It makes the strip as tall as the
 * card is wide at EVERY width the gate measures - 288px at 320, 330px at 1280 -
 * so the food dominates by construction rather than by tuning, and the lead
 * dish gets two thirds of it instead of a third. `QA/c6-verify.js` asserts the
 * comparison at all six widths, so the day somebody adds a line to the body
 * this stops passing rather than quietly inverting.
 *
 * A hairline, not a gap: three photographs read as one strip of that person's
 * cooking, where three separated squares read as three unrelated cards.
 */
.creator-card__food {
  display: grid;
  aspect-ratio: 1 / 1;
  grid-template-columns: 2fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 1px;
  background: var(--color-border);
}

.creator-card__food > :first-child { grid-row: 1 / span 2; }

/*
 * Fewer than three recipes fill the strip rather than leaving holes.
 *
 * A creator with one published recipe gets one full-width photograph, which is
 * the truth about them presented well. Reserving two empty squares beside it
 * would draw an absence nobody asked to see - and a lead tile spanning two rows
 * with nothing in the second column would leave exactly that.
 */
.creator-card__food:has(> :only-child) {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
}

.creator-card__food:has(> :first-child:nth-last-child(2)) {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr;
}
.creator-card__food:has(> :first-child:nth-last-child(2)) > :first-child { grid-row: auto; }

.creator-card__tile {
  position: relative;
  display: block;
  /* The tile fills the cell the strip gives it. The strip owns the ratio, so a
     lead dish and the two beside it are different sizes and the same shape of
     decision - and a photograph and a plate placeholder occupy the same box. */
  min-block-size: 0;
  overflow: hidden;
  text-decoration: none;
}

.creator-card__tile-media {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  background: var(--color-surface-sunken);
}

/*
 * The plate placeholder fills the cell too.
 *
 * `RH.plateHtml(recipe, 'tile')` carries `.rh-plate--tile`, which declares a
 * 1:1 aspect ratio - correct on a shelf, wrong here, where the LEAD cell is
 * twice as tall as the two beside it. Left alone it would draw a square inside
 * a tall cell and the strip would step wherever a creator's newest dish has no
 * photograph, which is exactly the mixed case a placeholder exists to survive.
 */
.creator-card__tile-media.rh-plate {
  aspect-ratio: auto;
  block-size: 100%;
  display: grid;
  place-items: center;
}

/*
 * The dish's name, on the dish.
 *
 * A strip of unlabelled squares is decoration. The title is the link's
 * accessible name AND is drawn, over a scrim that is dark enough for white
 * type on the brightest photograph in the catalogue.
 *
 * ONE LINE WITH AN ELLIPSIS, deliberately. Two lines of type over a 90px tile
 * covers the food this card exists to show, and the full title is one tap away
 * on the recipe itself - the accessible name is never truncated, only the
 * painting is.
 */
.creator-card__tile-title {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  padding: var(--space-4) var(--space-2) var(--space-2);
  background: linear-gradient(to top, rgba(20, 16, 13, .82), rgba(20, 16, 13, 0));
  color: var(--color-text-on-media);
  font: var(--type-caption);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
 * No food yet, said in words.
 *
 * FR-009 lists every active account and C-6 did not narrow that, so a member
 * who has published nothing is still here and still findable by name. What is
 * NOT done is dressing the absence up: no placeholder strip, no "coming soon",
 * no reserved media box implying a photograph that does not exist.
 */
.creator-card__nofood {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
  color: var(--color-text-muted);
  font: var(--type-caption);
}

/* -- 2. THE CREATOR ------------------------------------------------------- */

.creator-card__body {
  display: grid;
  gap: var(--space-2);
  padding: var(--space-4);
}

.creator-card__identity {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* A long display name shortens the name, never pushes the avatar off. */
  min-inline-size: 0;
}

.creator-card__names { min-inline-size: 0; }

.creator-card__name {
  margin: 0;
  font: var(--type-label);
}

.creator-card__name a { color: inherit; }

.creator-card__handle {
  margin: 0;
  color: var(--color-text-muted);
  font: var(--type-caption);
}

/*
 * Recipes first, followers second.
 *
 * Both are counts on real queries - published public recipes, and follow edges
 * whose other end is an active account. There is no third figure, because
 * there is no third figure this product can compute honestly at list scale.
 */
.creator-card__facts {
  margin: 0;
  color: var(--color-text-muted);
  font: var(--type-caption);
}

.creator-card__hints {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
}

/*
 * A hint, not a badge.
 *
 * Every one of these came out of that creator's own published recipes. The
 * styling is deliberately quiet - a filled, coloured chip would read as an
 * award, and section 9 forbids inventing authority.
 */
.creator-card__hint {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  color: var(--color-text-muted);
  font: var(--type-caption);
}

.creator-card__bio {
  margin: 0;
  color: var(--color-text-muted);
  font: var(--type-body-sm);
  text-wrap: pretty;
}

/* -- 3. THE ACTION -------------------------------------------------------- */

.creator-card__actions {
  display: flex;
  gap: var(--space-2);
  margin-block-start: var(--space-1);
}

/* Empty for a guest and for a card with nothing to do. The margin above would
   otherwise print as a gap under the bio on every card a guest sees. */
.creator-card__actions:empty { display: none; }

/* -- LOADING -------------------------------------------------------------- */

.creator-card--skeleton { pointer-events: none; }

/* The same three cells the real strip has, so nothing jumps when the food
   replaces them. */
.creator-card__skeleton-tile {
  display: block;
  min-block-size: 0;
  background: var(--color-surface-sunken);
  animation: rh-skeleton-pulse 1.6s var(--ease-out, ease-in-out) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .creator-card__skeleton-tile { animation: none; }
}

/* ==========================================================================
   THE EDITOR AS A TASK. C-8, closing FIND-P2-10.
   ==========================================================================

   Two blocks, and one rule shared between them: nothing was deleted. The
   readiness panel is the same `.readiness` the recipe page draws, given a
   modifier so it can sit at the top of a form rather than beside a dish; the
   advanced region is the same disclosure `/recipes` and `/me` already use.
   ========================================================================== */

.editor-readiness {
  margin-bottom: var(--space-6);
}

/*
 * The rows are LINKS here and were plain text on the recipe page, because on
 * this screen the control that fixes each one is a few hundred pixels away.
 *
 * THE WHOLE ROW, not the two words at the start of it. The first version linked
 * only `.readiness__what`, and the responsive matrix failed it at 320, 360, 390
 * and 430: six targets 21px tall against a 44px floor, the smallest 37px wide.
 * A four-character link cannot be padded to 44 square without reading as a
 * button, so the row became the target instead - which is also what it is.
 */
.readiness__jump {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  min-height: var(--tap-min);
  align-content: center;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius-sm, 4px);
}
.readiness__jump:hover .readiness__what { opacity: 0.8; }
/* Underlined only on the noun, and only on hover: six underlined phrases in a
   panel read as a navigation list rather than as what is left to do. */
.readiness__jump .readiness__what { border-bottom: 1px solid currentColor; }
.readiness__jump:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}
/* The row holds one child now, so it stops being the flex line itself. */
.editor-readiness .readiness__item { display: block; padding-block: 0; }

/* -- the rest of the model, behind one door -------------------------------- */

.editor-advanced { margin-block: var(--space-6); }

/*
 * The toggle reuses `.search-bar__filters` rather than restating it. It is the
 * same control doing the same job on a third screen, and the C-4 comment beside
 * that class records the 320px stress failure its `nowrap` and `flex-shrink: 0`
 * exist to prevent - inheriting the class inherits the fix.
 */
.editor-advanced__toggle { width: auto; }

.editor-advanced__panel {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider);
}
/* Focused programmatically when the door opens; the ring belongs on the
   button that was pressed, not on the container that received focus. */
.editor-advanced__panel:focus { outline: none; }

/* ==========================================================================
   CREATOR TOOLS, AND THREE DESTINATIONS THAT HAD NO WAY IN ON A PHONE.
   ==========================================================================

   The Stage C task was "group /media-lab and /stats". Measuring the move first
   found something larger and identical in all three cases: the header's `עוד`
   overflow menu is `display: none` below 720px, and it was the ONLY entry point
   to /media-lab, /stats, /users and /comments. Four destinations, two of them
   carrying graded capabilities, with no route in the interface at 320, 390, 430
   or 719.

   Each one is placed where it belongs rather than in one new menu: the two
   tools on the profile, creator discovery on Explore, the comment search beside
   the recipe search. The overflow menu keeps all four for a desktop.
   ========================================================================== */

.creator-tools { margin-bottom: var(--space-7); }

.creator-tools__title {
  margin: 0 0 var(--space-3);
  font: var(--type-label);
  color: var(--color-text-muted);
}

.creator-tools__row {
  display: grid;
  gap: var(--space-3);
}
@media (min-width: 560px) {
  .creator-tools__row { grid-template-columns: 1fr 1fr; }
}

/*
 * A card rather than a button, because each one is a room. The whole card is
 * the target and it is well past the 44px floor at every width - the readiness
 * rows in the editor are the record of what happens when only the words inside
 * a row are the link.
 */
.creator-tools__tool {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-height: 64px;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
}
.creator-tools__tool:hover { border-color: var(--color-accent); }
.creator-tools__tool:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.creator-tools__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-avatar);
  background: var(--color-accent-soft);
  color: var(--color-accent-text);
}

.creator-tools__body { display: flex; flex-direction: column; min-width: 0; }
.creator-tools__name { font: var(--type-label); color: var(--color-text-strong); }
.creator-tools__sub {
  font: var(--type-caption);
  color: var(--color-text-muted);
  text-wrap: pretty;
}

/* -- the two sibling links --------------------------------------------------

   Both are single links in their own paragraph, so `min-height` on the anchor
   is what carries the tap floor. `display: inline-flex` rather than `inline`,
   because an inline link takes its height from its line and the sweep is right
   to refuse a 21px target on a phone. */

.search-sibling,
.explore__people { margin: 0 0 var(--space-5); }

.search-sibling a,
.explore__people a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  font: var(--type-body-sm);
  color: var(--color-accent-text);
  text-decoration: none;
}
.search-sibling a:hover,
.explore__people a:hover { text-decoration: underline; }
.search-sibling a:focus-visible,
.explore__people a:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm, 4px);
}
.search-sibling .rh-icon,
.explore__people .rh-icon { color: var(--color-text-muted); }


/*
 * THE ALLERGEN REVIEW CHECKBOXES ARE A 44px TARGET.
 *
 * Measured at 25px tall on a phone - seven of the nine allergens below the
 * minimum, on the one administrative screen whose mis-tap is somebody's
 * allergic reaction. The member-facing equivalent on `/me` is already 44px, so
 * the product knows how; this screen was built later and did not inherit it.
 */
.admin-allergens { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.admin-allergen {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: var(--tap-min);
  padding-inline: var(--space-3);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-input);
  cursor: pointer;
}
.admin-allergen input { inline-size: 20px; block-size: 20px; }

/* ==========================================================================
   THE IMMERSIVE FEED SHELL
   ==========================================================================

   A dedicated composition for /feed, not another override against the card.

   THE MEASURED PROBLEM. Before this, /feed at 1280 scrolled the DOCUMENT, had
   no snap, and the media occupied 23.9% of the canvas with its optical centre
   170px right of the viewport centre - because an explanatory aside and a
   content column divided the page between them. The reference topology is the
   inverse: media optically centred in the WHOLE viewport, application chrome
   arranged around it. Evidence in docs/60.

   THE ONE RULE EVERYTHING ELSE SERVES. Stage geometry is a function of the
   VIEWPORT, never of the content. A long title, a long creator name or a
   five-digit like count render inside fixed geometry. If content could move the
   stage, scrolling between items would feel like a list of differently shaped
   cards instead of one surface.
   ========================================================================== */

/*
 * The document must not page the feed. `overflow:hidden` on the shell alone
 * left the DOCUMENT scrollable, so a wheel gesture near the edge moved the page
 * instead of the feed - which is the exact behaviour the internal scroller
 * exists to replace.
 */
/*
 * THE FEED IS A DARK ROOM, AT EVERY WIDTH.
 *
 * `demo/README.md` lists this under MANDATORY and names the value:
 * "הפיד כהה בכל רוחב... הכהה הוא #14100e (לילה מהפלטה), לא שחור."
 *
 * Scoped by `:has(.feedshell)` rather than applied to the application, because
 * the same document says the rest stays warm paper. The objection the product
 * raised when it removed this - that a dark shell reads as a fourth design
 * direction - is answered by the scope rather than ignored.
 */
html:has(.feedshell),
body:has(.feedshell) {
  background: var(--fl-feed-ground);
  overflow: hidden;
  /*
   * The bottom padding that reserves room for the mobile tab bar has to go on
   * this route. The shell is already 100dvh, so an extra 80px below it made the
   * DOCUMENT 1006px tall in a 936px viewport - and it really scrolled, 70px,
   * even with `overflow:hidden` on the body, because it is the root element
   * that scrolls. The tab bar overlays the immersive feed instead, and the
   * lower content zone carries its own safe-area padding so nothing hides
   * behind it.
   */
  padding-block-end: 0;
}

.feedshell {
  /*
   * `main` carries 40px top and 64px bottom padding for the ordinary document
   * layout, and the shell is a `<main>`. That inset the scroller by 104px, so
   * the item was 832px inside a 936px viewport and the stage was sized from a
   * height it did not have. The immersive shell IS the viewport.
   */
  padding: 0;
  block-size: 100dvh;
  overflow: hidden;
  display: grid;
  grid-template-columns: 1fr;
  /*
   * THE FEED IS A DARK ROOM. PRODUCT OVERRULED THE PARAGRAPH THAT WAS HERE.
   *
   * What stood here argued that a dark shell was "a fourth design direction
   * imported along with the reference product's topology", and that darkness is
   * a treatment for MEDIA rather than the application's colour. That is a real
   * risk and the argument was not weak.
   *
   * The design package's handover lists the dark feed under MANDATORY - the tier
   * whose rule is "deviation only with a documented TECHNICAL reason" - and names
   * the value: "הפיד כהה בכל רוחב... הכהה הוא #14100e (לילה מהפלטה), לא שחור".
   * The objection above is a design judgement, not a technical constraint, so it
   * does not clear that bar. Product was shown both positions and chose the
   * package.
   *
   * The objection is ANSWERED rather than ignored, and the answer is the scope.
   * The same document says "שאר המסכים נייר חם", so this is one route wearing a
   * theatre while the other twenty stay paper - not an application theme.
   *
   * AND IT HAS TO BE THIS DECLARATION. The first attempt added
   * `background: var(--fl-feed-ground)` at the TOP of this rule and left the
   * line below untouched, so the later declaration won and the shell stayed
   * `#faf8f5`. `html` and `body` went dark, the shell that covers them did not,
   * and a measurement that read only `body` reported success. The lesson is the
   * one this file keeps teaching: measure the element a person actually sees.
   */
  background: var(--fl-feed-ground);
  color: var(--color-text);
}

/*
 * ONE ITEM IS ONE PAGING UNIT, and it was not.
 *
 * The stage was a three-row grid - modes, scroller, load-more - so the scroller
 * got what was left (783px in a 936px viewport) while the media stage was sized
 * from the viewport (calc(100dvh - 32px) = 904). A 904px stage inside a 783px
 * snap item: the snap unit, the item and the media described three different
 * geometries, and a wheel settled 783 while the eye tracked 904.
 *
 * The chrome overlays instead. The scroller is the whole stage, the item is the
 * whole scroller, and the media derives its height from the item rather than
 * from the viewport - so the three cannot drift apart again. Load More keeps its
 * place in the DOM and its keyboard reachability; it simply stops taking 121px
 * out of every item for ever.
 */
.feedstage {
  position: relative;
  min-block-size: 0;
  display: grid;
  grid-template-rows: 1fr;
  /*
   * The section still carries `feed page-feed` so the existing feed JavaScript
   * and its tests keep their hooks, and those legacy rules cap the column at
   * 620px for the document layout. The immersive shell spans the viewport, so
   * the cap is released here rather than by removing classes other code binds
   * to.
   */
  max-inline-size: none;
  inline-size: 100%;
  padding: 0;
}
.feedscroller { max-inline-size: none; }

/*
 * FOR YOU / FOLLOWING as content modes, not form controls.
 *
 * They were two 398x46 pills above a card. Here they are quiet chrome over the
 * top of the stage, inside the safe area, and they no longer consume a band of
 * the composition that the media could have used.
 */
.feedstage__modes {
  position: absolute;
  /*
   * PINNED TO THE EDGE AND PADDED INWARDS, SO IT CAN CARRY A SCRIM.
   *
   * The top of a photograph had no protection at all - the lower scrim covers
   * the bottom 62% - and the unselected mode label measured 2.36:1 over a
   * bright dish. Offsetting the box by the safe area left a strip above it that
   * no gradient could reach, so the offset became padding instead: the box
   * starts at the edge, the text still clears the notch, and the darkening
   * fades out below the labels.
   */
  inset-block-start: 0;
  inset-inline: 0;
  padding-block: max(var(--space-3), env(safe-area-inset-top, 0px)) var(--space-8);
  background: linear-gradient(to bottom, rgb(0 0 0 / 0.6), rgb(0 0 0 / 0));
  z-index: 3;
  display: flex;
  justify-content: center;
  gap: var(--space-4);
  pointer-events: none;
}
.feedmode {
  pointer-events: auto;
  min-block-size: var(--tap-min);
  padding-inline: var(--space-3);
  border: 0;
  background: transparent;
  /*
   * The unselected mode is DEMOTED, not dimmed into the photograph: the pair is
   * the only way to change what the Feed is showing, so both have to be read.
   * The selected one is white with a rule under it; this one is the same ink at
   * lower opacity, which keeps the hierarchy without taking it below the floor.
   */
  color: rgb(247 243 240 / 0.82);
  font-size: var(--text-md);
  font-weight: 700;
  cursor: pointer;
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.55);
}
.feedmode[aria-selected="true"] {
  color: var(--fl-feed-on-media, #f7f3f0);
  box-shadow: inset 0 -2px 0 currentColor;
}

/*
 * THE STATUS SURFACES OVERLAY TOO.
 *
 * `#feed-notice` and `#feed-empty` are hidden most of the time, but a hidden
 * element still holds its grid row and still contributes margins - together
 * they inset the scroller by 104px, so the stage was sized from a viewport it
 * did not actually have and the social rail was placed 34px from the media
 * instead of 16. Overlaying them makes the scroller the only in-flow child, so
 * the item IS the viewport.
 */
.feedstage > #feed-notice,
.feedstage > #feed-empty {
  position: absolute;
  inset-inline: var(--space-4);
  inset-block-start: 25%;
  z-index: 4;
  margin: 0;
  text-align: center;
}

.feed__more {
  position: absolute;
  inset-block-end: calc(var(--space-3) + var(--tabbar-h, 65px) + env(safe-area-inset-bottom, 0px));
  inset-inline-start: 50%;
  transform: translateX(50%);
  z-index: 3;
  display: grid;
  justify-items: center;
  gap: var(--space-1);
  pointer-events: none;
}
.feed__more > * { pointer-events: auto; }

.feedscroller {
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
}
.feedscroller::-webkit-scrollbar { display: none; }

.fitem {
  block-size: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  position: relative;
  display: grid;
  place-items: center;
}

.fitem__stage {
  /*
   * THE MEDIA THEATRE. This is where dark is allowed to be a surface.
   *
   * The ground shows as letterboxing behind a contained landscape photograph
   * and as the frame around a portrait one, so it is the one region that has to
   * be dark for the food to read - and it is `--color-surface-inverse`, the ink
   * Warm Table already uses for its inverse surface, rather than a private
   * near-black.
   *
   * The elevation is what separates the theatre from the warm canvas around it
   * on desktop, where the two now meet.
   */
  background: var(--fl-media-ground);
  box-shadow: var(--shadow-media);
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  display: grid;
  place-items: center;
  overflow: hidden;
}

.fitem__media {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}

/*
 * SOURCE-AWARE FIT. Portrait may cover, because the frame IS the subject.
 * Landscape is contained on a neutral stage - a cooking scene cropped to 9:16
 * loses the pan, the hands and half the technique, and the directive forbids
 * destroying cooking information to force a shape.
 */
.fitem__media img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
}
.fitem--portrait .fitem__media img { object-fit: cover; }

/*
 * The lower content zone: lower RIGHT for Hebrew, over a scrim rather than an
 * opaque card. The scrim is the readability treatment; a white panel under the
 * photograph is the card this redesign exists to remove.
 */
.fitem__lower {
  /*
   * THE OVERLAY CARRIES ITS OWN INK.
   *
   * It used to inherit it from the shell, which was white because the shell was
   * black. The shell is Warm Table now, so inheritance handed the title and the
   * creator dark ink and painted them onto a photograph - invisible, and only
   * because a colour three levels up changed. Layer 3 states its own colour.
   */
  color: var(--fl-media-on);
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  z-index: 2;
  display: grid;
  gap: var(--space-2);
  /*
   * LOWER RIGHT, which in RTL is inline-START.
   *
   * Written as `end` first, which is physical LEFT in RTL - so the Hebrew
   * creator, title and CTA rendered 16px from the media's left edge, directly
   * against the social rail, and the two competed for one reading zone. Measured
   * at x=402 with the media spanning 386..895.
   */
  justify-items: start;
  text-align: start;
  padding: var(--space-6) var(--space-4)
           calc(var(--space-6) + var(--tabbar-h, 65px) + env(safe-area-inset-bottom, 0px));
  /*
   * The rail is on physical LEFT = inline-END, so the space reserved for it is
   * on that side. Reserving it on inline-start pushed the text away from the
   * side it was supposed to sit on AND left it overlapping the rail.
   */
  padding-inline-end: calc(var(--space-4) + 64px);
  /* The gradient moved to `.fitem__scrim`, which is taller than this zone so
     the fade lands on photograph instead of on the first line of text. */
  pointer-events: none;
}

.fitem__scrim {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  /* Taller than the content it protects: the top third of this box is fade,
     and no text is placed in it. */
  block-size: 62%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top,
    rgb(0 0 0 / 0.85) 0%,
    rgb(0 0 0 / 0.6) 35%,
    rgb(0 0 0 / 0.25) 65%,
    rgb(0 0 0 / 0) 100%);
}
/*
 * THE SCOPE HAS NO BOX.
 *
 * `AppLayout` wraps the feed and its navigation in an element only so that
 * `data-direction="night"` has somewhere to live. A real block container there
 * would be a new containing block for a `100dvh` shell and a `position: sticky`
 * tab bar - the two things on this route most sensitive to one.
 *
 * `display: contents` removes the box and keeps the element in the tree, and
 * custom properties inherit through it either way, which is the whole reason
 * the attribute works from here.
 */
.rh-shellframe { display: contents; }

/*
 * ============================================================================
 * THE CINEMATIC LANDING
 * ============================================================================
 *
 * Geometry from the demo's `.hero`. The landing was 2,426px of prose carrying
 * one image - the logo - on a product whose subject is food, and the gap table
 * had it as "קיימים בצורה טקסטואלית · עיצוב מחדש" all along.
 *
 * It BREAKS OUT of the container it is rendered in. The landing sits inside
 * the ordinary page shell, and a full-bleed stage inside a 980px column is a
 * letterboxed photograph. The negative margin is the same trick the demo uses
 * (`margin: 0 calc(-1 * var(--gutter-screen))`), widened to the viewport
 * because this container is centred rather than gutter-padded.
 */
.hero {
  position: relative;
  overflow: hidden;
  block-size: 100dvh;
  min-block-size: 540px;
  /* Out of the column and across the viewport, without a horizontal
     scrollbar: `100vw` includes the scrollbar's width and `100%` of the
     viewport-sized ancestor does not. */
  margin-inline: calc(50% - 50vw);
  inline-size: 100vw;
  max-inline-size: 100vw;
  background: var(--fl-feed-ground);
  color: var(--fl-media-on);
  isolation: isolate;
}

.hero__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* §3, the same crop window every other frame uses on this 4:5 set. */
  object-position: center 40%;
  transition: opacity 1.4s var(--ease-in-out, ease-in-out);
}
@media (prefers-reduced-motion: reduce) {
  .hero__img { transition: none; }
}

/*
 * Dark at BOTH ends and thin in the middle: the top carries the wordmark and
 * the sign-in link, the bottom carries the title and the buttons, and the
 * middle is the dish. A single top-down scrim would have protected the words
 * at one end and dimmed the food at the other.
 */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top,
    rgb(10 7 6 / 0.90) 0%,
    rgb(10 7 6 / 0.42) 42%,
    rgb(10 7 6 / 0.14) 72%,
    rgb(10 7 6 / 0.40) 100%);
}

.hero__top {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
}
/* The wordmark is on a photograph here, not on paper. */
.hero__top .brand { color: var(--fl-media-on); }
.hero__top .brand__word { text-shadow: 0 1px 6px rgb(0 0 0 / 0.45); }

.hero__body {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 2;
  display: grid;
  gap: var(--space-3);
  justify-items: center;
  text-align: center;
  padding: 0 var(--space-5) calc(var(--space-8) + env(safe-area-inset-bottom, 0px));
}

.hero__title {
  margin: 0;
  font-weight: 700;
  /* Fluid rather than a breakpoint: this line is the whole screen at 390 and
     an opening at 1440, and it should not step. */
  font-size: clamp(2.25rem, 6vw, 3.625rem);
  line-height: 1.1;
  letter-spacing: -0.01em;
  text-shadow: 0 2px 14px rgb(0 0 0 / 0.35);
}

.hero__sub {
  margin: 0;
  font-size: clamp(1rem, 2vw, 1.25rem);
  max-inline-size: 36ch;
  text-wrap: pretty;
  opacity: .94;
  text-shadow: 0 1px 6px rgb(0 0 0 / 0.35);
}

.hero__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; justify-content: center; }
.hero__note { margin: 0; font-size: var(--text-xs); opacity: .72; }

.hero__dots {
  position: absolute;
  inset-inline: 0;
  inset-block-end: var(--space-3);
  z-index: 2;
  display: flex;
  gap: 6px;
  justify-content: center;
}
.hero__dots span {
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: rgb(255 255 255 / 0.35);
  transition: background var(--speed) var(--ease-out);
}
.hero__dots span.is-on { background: #fff; }

/* Six dishes under the fold. Square, because a mosaic of mixed ratios is a
   collage and this is a wall. */
.mosaic {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
  inline-size: 100%;
  max-inline-size: 860px;
  margin-inline: auto;
  margin-block: var(--space-7) var(--space-6);
}
/* The closing call to action. Centred and alone: a second control beside it
   would make a visitor choose instead of act. */
/*
 * R5-1. THE ROOM EVERYTHING UNDER THE HERO STANDS IN.
 *
 * 1120 rather than the app shell's own width: this page is a single column of
 * argument rather than a working surface, and a measure wider than about 70
 * characters stops being readable prose. The gutter is what keeps a card off
 * the glass on a laptop and a phone alike.
 */
/*
 * ============================================================================
 * R5-3. DIET PREFERENCES ARE A CONSUMER SCREEN, NOT A CONFIGURATION FORM
 * ============================================================================
 *
 * Three groups - allergens, diets, cuisines - rendered as the browser's own
 * `fieldset`: a hairline with the legend cutting through it, square native
 * checkboxes, rows packed tight. The review put it exactly: a consumer screen
 * that looks like an admin one, on the screen that is most personal in this
 * product. What a person eats is not a config key.
 *
 * THE MARKUP IS NOT TOUCHED, and that is the point of doing this in CSS.
 * `fieldset` and `legend` are the correct grouping semantics for a set of
 * related checkboxes - a screen reader announces the legend before each choice
 * inside it - and replacing them with divs and a heading would trade a real
 * affordance for a visual one. Everything below is paint.
 *
 * `box-shadow: inset` rather than `border`, because a legend NOTCHES a
 * fieldset's border by specification. An inset shadow draws the same hairline
 * and the legend sits above it as a heading, which is what the review asked
 * for: a label heading rather than a legend on a line.
 */
#preferences-form fieldset {
  border: 0;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: inset 0 0 0 1px var(--color-border);
  padding: var(--pad-card, var(--space-5));
  margin: 0;
}

#preferences-form legend {
  /* `float` takes it out of the border flow entirely, so it reads as the
     card heading rather than as a label interrupting a line. */
  float: inline-start;
  inline-size: 100%;
  padding: 0;
  margin-block-end: var(--space-2);
  font: var(--type-heading);
  color: var(--color-text);
}
/* The float is closed before the choices lay out. */
#preferences-form legend + * { clear: both; }

/*
 * THE CHOICES ARE THE CHIPS THE SEARCH FILTERS ALREADY USE.
 *
 * `.u-choice` was `display: block; width: 100%` - a full-width row per option,
 * three columns of them, each a line of text beside a native square. As a chip
 * it is the same control a member has already met when filtering, it carries
 * the 44px chip floor, and being SELECTED looks like being selected rather
 * than like a tick in a box.
 */
#preferences-form .u-columns {
  /* Not columns any more: chips flow. `column-count` would slice a wrap. */
  column-count: auto;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

#preferences-form .u-choice {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  inline-size: auto;
  margin: 0;
  /* 44 and not 48: this is a chip, and `--tap-chip` is the token that says
     which of the two floors governs. */
  min-block-size: var(--tap-chip);
  padding-inline: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: var(--text-sm);
  transition: background-color var(--speed) var(--ease-out),
              border-color var(--speed) var(--ease-out);
}

#preferences-form .u-choice:hover { border-color: var(--color-accent); }

/*
 * THE NATIVE BOX IS CLIPPED, NOT REMOVED.
 *
 * `display: none` would take the checkbox out of the accessibility tree and
 * out of the tab sequence, which would make this a picture of a control. The
 * clip keeps it operable, focusable and announced; the label around it is what
 * a person sees and presses.
 */
#preferences-form .u-choice input[type="checkbox"] {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Selected, and it says so twice: colour and a mark. Colour alone is not a
   state every reader can read. */
#preferences-form .u-choice:has(input[type="checkbox"]:checked) {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-text);
  font-weight: var(--weight-bold);
}
#preferences-form .u-choice:has(input[type="checkbox"]:checked)::before {
  content: "\2713";
  font-size: var(--text-xs);
  line-height: 1;
}

/* The focus ring belongs to the chip, because the box it would sit on is a
   clipped pixel. */
#preferences-form .u-choice:has(input[type="checkbox"]:focus-visible) {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}

.landing-shell {
  max-inline-size: 1120px;
  margin-inline: auto;
  padding-inline: var(--space-6);
}
@media (max-width: 900px) {
  .landing-shell { padding-inline: var(--space-5); }
}

/*
 * AND THE TWO COLUMNS STOP FLOATING PAST EACH OTHER.
 *
 * `align-items: center` on columns of different heights pushes each one to its
 * own vertical middle, so the words and the diagram shared no horizon - part
 * of what read as three unrelated islands. `start` gives them one head, and
 * the diagram is centred back on its own axis because it is a figure rather
 * than a paragraph.
 */
@media (min-width: 720px) {
  .landing { align-items: start; }
  .landing__art { align-self: center; justify-self: center; max-inline-size: 420px; }
}

.landing__close {
  display: flex;
  justify-content: center;
  padding-block: var(--space-7) var(--space-8);
}

.mosaic img {
  inline-size: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: center 40%;
  border-radius: var(--radius-md);
  display: block;
  background: var(--media-placeholder);
}

.fitem__lower > * { pointer-events: auto; }

/*
 * THE TOP OF THE STAGE, WHERE A CLAIM ABOUT THE READER GOES.
 *
 * The scrim above protects the bottom 62% and nothing protects the top, which
 * is deliberate - the only thing placed here is `.rh-match`, and that carries
 * its own near-opaque ground and its own shadow precisely so it can sit on an
 * unprotected photograph. Anything that needs the scrim belongs in
 * `.fitem__lower` instead.
 *
 * `z-index: 2` puts it in the same layer as the words rather than under the
 * gradient, and `pointer-events: none` keeps an empty slot - which is what this
 * is on every card today - from covering the top of the media and swallowing a
 * tap meant for the video.
 *
 * Inline-START, which in RTL is the RIGHT edge, where a Hebrew reader's eye
 * enters. The rail is on the other side and this never reaches it.
 */
.fitem__top {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 2;
  display: flex;
  justify-content: flex-start;
  padding: var(--space-4);
  pointer-events: none;
}
.fitem__top > * { pointer-events: auto; }

/*
 * R3-1. THE PLATE CAPTION AND THE CARD WORDS WERE THE SAME PIXELS.
 *
 * Measured on a card with no photograph, at 390: the caption occupied
 * 518-557 and the safety line occupied 534-557. Not close - the same band,
 * the whole of it, with the title clipped at its foot as well. Two layers
 * writing into one region, on the flagship screen, at both widths.
 *
 * It happens because the plate is not a card here. Everywhere else it is a
 * small framed region with nothing over it; in the feed it is stretched to a
 * 390x844 media theatre and its centred caption lands exactly where
 * `.fitem__lower` draws the creator, the title and the safety line.
 *
 * THE CAPTION GOES, AND ONLY IN THE FEED. The review offered the choice, and
 * this is the half that cannot collide again by construction: moving it puts
 * it somewhere else inside the same 40% band that `.fitem__lower` owns.
 *
 * Nothing is lost. The plate keeps its ring and its monogram, which is the
 * placeholder pattern; the card says the dish name underneath; and the whole
 * media element carries aria-hidden in the feed, so the caption was never
 * announced to a screen reader here in the first place. On the light tiles -
 * explore, search, favourites - the plate is small, nothing is over it, and
 * the caption stays.
 */
.fitem__media--plate .rh-plate__note { display: none; }

.fitem__creator {
  /*
   * THE HIT AREA GROWS, THE TEXT DOES NOT.
   *
   * The package is explicit about clickable meta: "אזור המגע מורחב ל-48px
   * בעזרת padding או pseudo-element, בלי להגדיל את הטקסט". A creator name
   * set to 48px would out-shout the recipe title beside it; padding gives a
   * thumb the same 48 without touching the type.
   */
  min-block-size: var(--tap-min);
  padding-block: var(--space-2);
  color: var(--fl-feed-on-media, #f7f3f0);
  font-weight: 700;
  text-decoration: none;
}
.fitem__creator:hover .fitem__creator-name { text-decoration: underline; }

.fitem__title {
  margin: 0;
  font-size: var(--text-lg);
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.fitem__title a { color: inherit; text-decoration: none; }

/*
 * SAFETY. Compact and integrated, never hidden.
 *
 * The topology audit recommends removing allergen status from the discovery
 * layer. Product safety policy overrides that: a declared conflict stays
 * prominent, a required unreviewed warning stays visible, and only the SIZE of
 * the treatment changed. It sits above Open Recipe so it cannot be missed on
 * the way to the action.
 */
.fitem__safety {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: #ffd9a8;
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.6);
}

.fitem__hint {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--fl-feed-muted, #b8ada6);
}

/*
 * THE PROVENANCE CREDIT.
 *
 * Quieter than the hint and quieter than the creator, because it is an
 * attribution rather than something a reader is being asked to act on. It
 * shares the hint's muted colour and sits directly under the creator, so the
 * two facts about WHO read as one block instead of competing.
 */
.fitem__source {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--fl-feed-muted, #b8ada6);
}
.fitem__source span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * OPEN RECIPE: THE STRONGEST FLAYVO ACTION ON THE FEED, IN FLAYVO'S OWN CLAY.
 *
 * It was `var(--color-primary, #e8622c)`. There is no `--color-primary` in this
 * product - the name is from an older palette - so what actually rendered was
 * the FALLBACK: #e8622c, a colour that appears nowhere else and belongs to no
 * ramp. The token-integrity gate could not see it, because a `var()` with a
 * fallback is not an undefined reference; the colour audit could, because it
 * reads what the browser resolved.
 *
 * `--color-accent` is clay-500, the same accent the product's primary buttons
 * use everywhere else, which is what makes this control read as Flayvo's rather
 * than as a generic orange pill.
 */
.fitem__open {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap-min);
  padding-inline: var(--space-5);
  border-radius: var(--radius-pill);
  /*
   * ON-MEDIA GHOST, NOT PRIMARY. The design review counted the cost: a solid
   * accent pill on every card is N primaries per screen, and the feed measured
   * ELEVEN of them at 390 - ten cards and the skip link. A primary is the one
   * thing on a screen; eleven of them is none of them, and each one competed
   * with the photograph it was sitting on.
   *
   * This is the package's `.rh-btn--onmedia` - a translucent white wash, a white
   * hairline and a blur - restated here rather than by adding the class, because
   * `.fitem__open` is a link in a card rather than a `.rh-btn` and inheriting the
   * button's box would change its geometry along with its weight.
   *
   * It stays a LABELLED control and stays first in the eye's path. What changes
   * is that it no longer claims to be the most important thing on a screen whose
   * most important thing is the food.
   */
  background: rgb(255 255 255 / 0.18);
  color: #fff;
  border: 1px solid rgb(255 255 255 / 0.55);
  /* The wash alone is not enough over a bright dish; the blur is what makes a
     translucent control legible on an image it does not control. */
  backdrop-filter: blur(8px);
  font-weight: 700;
  text-decoration: none;
}
/* Hover inverts rather than brightening, which is the package's own behaviour
   and the only state where this control is allowed to be loud. */
.fitem__open:hover,
.fitem__open:focus-visible { background: #fff; color: var(--rh-ink-900, #241d18); }

.frail {
  position: absolute;
  /*
   * LEFT of the media, which in RTL is the inline-END side.
   *
   * Written as `inset-inline-start` first, which put the rail on the RIGHT at
   * every width - measured at x=282 in a 390 viewport. The logical properties
   * are correct for the writing mode and the writing mode is what flips them:
   * inline-start in RTL IS the right edge.
   */
  inset-inline-end: var(--space-3);
  inset-block-end: calc(var(--space-6) + var(--tabbar-h, 65px) + env(safe-area-inset-bottom, 0px));
  z-index: 3;
  display: grid;
  gap: var(--space-4);
  justify-items: center;
  /*
   * THE RAIL CARRIES ITS OWN PROTECTION, BECAUSE IT SITS ON THE FOOD.
   *
   * On a phone the media fills the viewport and the rail is over it, above the
   * lower scrim's reach - so its counts and its follow control had nothing but
   * photograph behind them. Sampled over a bright dish they read 1.82:1 and
   * 1.38:1 against a 4.5 floor; the screenshot looked fine, which is exactly
   * how a legibility defect survives review.
   *
   * A column rather than a chip per control: chips would turn five quiet glyphs
   * into five buttons competing with the dish, and the reference product has no
   * such thing. The gradient is strongest at the screen edge, where the icons
   * are, and gone by the inner edge, so it reads as a vignette rather than a
   * panel.
   *
   * Desktop does not need it - the rail is beside the media on the dark canvas
   * there and already measures above 12:1 - so the treatment stops at 720.
   */
  /*
   * The generous block padding is the vignette's room to fade.
   *
   * A radial background is clipped to its element's box, so if it has not
   * reached zero by the box edge the clip IS an edge - a grey rectangle over
   * the food, which is what the first attempt drew. The box is therefore taller
   * than the controls it holds, and the gradient reaches zero exactly at the
   * boundary.
   */
  padding: var(--space-8) var(--space-3);
  /*
   * RADIAL, so it has no edges.
   *
   * A linear gradient across the rail's box protected the glyphs and drew a
   * visible rectangle on the photograph: hard top and bottom edges where the
   * box ended, which reads as a panel bolted over the food. A radial anchored
   * to the screen edge falls off in every direction at once, so the darkening
   * ends where the photograph is still photograph.
   */
  background: radial-gradient(125% 50% at 0% 50%,
    rgb(0 0 0 / 0.92) 0%, rgb(0 0 0 / 0.72) 45%, rgb(0 0 0 / 0) 100%);
}

@media (min-width: 720px) {
  .frail {
    padding: 0;
    background: none;
  }
}
.frail__creator { display: block; }
.frail__btn {
  min-inline-size: 48px;
  min-block-size: 48px;
  display: grid;
  place-items: center;
  gap: 2px;
  border: 0;
  background: transparent;
  color: var(--fl-feed-on-media, #f7f3f0);
  cursor: pointer;
  filter: drop-shadow(0 1px 3px rgb(0 0 0 / 0.6));
}
/*
 * The COUNT stays white even when the control is on.
 *
 * `aria-pressed="true"` turns the button's colour to the brand orange, which is
 * right for the heart and wrong for the number beside it: sampled over a
 * photograph the orange digits measured 1.39:1. The icon may carry the accent -
 * it is a graphic, and 3:1 is its bar - but the digits are text.
 */
.frail__count {
  font-size: var(--text-xs);
  color: var(--fl-feed-on-media, #f7f3f0);
}
.frail__btn[aria-pressed="true"] { color: var(--fl-feed-on-accent, #ff9166); }
.frail__btn--quiet { opacity: 0.65; }
/*
 * 44px, the product's touch floor. It was 32px - small enough to miss on the
 * one control that changes a social relationship, and inside a rail whose other
 * controls are all 48px.
 */
.frail__follow {
  min-block-size: var(--tap-min);
  padding-inline: var(--space-3);
  border-radius: 999px;
  border: 1px solid currentColor;
  /*
   * The one control in the rail that carries WORDS carries its own backing.
   *
   * It sits at the top of the rail, furthest from the centre of the vignette,
   * and measured 3.56:1 there over a bright dish. Text needs 4.5, and a pill
   * with a filled ground is what the reference product uses for this control
   * anyway - it is the only one that is a commitment rather than a tap.
   */
  background: rgb(0 0 0 / 0.6);
  color: var(--fl-feed-on-media, #f7f3f0);
  font-size: var(--text-xs);
  cursor: pointer;
}

/*
 * NOT INTERESTED, AND THE WAY BACK FROM IT.
 *
 * Keyed off `is-hidden`, which is the class `social.js` toggles on every card
 * family. This block was written against `[data-hidden="true"]`, an attribute
 * nothing in the product sets - so the item never collapsed, the undo never
 * appeared, and "not interested" looked like it had done nothing at all.
 *
 * The item is not removed from the queue: it keeps its viewport slot with the
 * media dimmed behind the undo, because collapsing a full-screen item would
 * scroll the reader somewhere they did not ask to be. The next feed request is
 * what actually drops it.
 */
.fitem__hidden-note { display: none; }
.fitem.is-hidden .fitem__lower,
.fitem.is-hidden .frail { display: none; }
.fitem.is-hidden .fitem__media { opacity: 0.2; }
.fitem.is-hidden .fitem__hidden-note {
  display: block;
  position: absolute;
  inset-block-end: 40%;
  inset-inline: var(--space-6);
  z-index: 4;
  text-align: center;
  color: var(--fl-feed-on-media, #f7f3f0);
}

/*
 * A refused action, reported beside the control that was refused.
 *
 * Anchored to the rail rather than dropped into the layout: the rail is
 * absolutely positioned beside the media, so an in-flow error box would resize
 * it and move every icon under the reader's finger mid-press.
 */
.frail__error {
  position: absolute;
  inset-inline-start: 100%;
  inset-block-end: 0;
  inline-size: max-content;
  max-inline-size: 16rem;
  margin-inline-start: var(--space-2);
  font-size: var(--text-xs);
}
.frail__error[hidden] { display: none; }

/* -- desktop: navigation right, media centred in the whole viewport -------- */

/*
 * 720px, the same width the rail appears at.
 *
 * This block carries the rail's `position: fixed`, the stage variables and the
 * whole desktop composition, and it was still at 900 while `display: flex`
 * moved to 720 - so between 720 and 899 the rail was VISIBLE but still an
 * in-flow grid item, 366px tall, pushing the scroller down to a 539px item in a
 * 936px viewport. A breakpoint split across two rules is a composition that
 * exists at no width.
 */
@media (min-width: 720px) {
  /*
   * The rail is a COLUMN of the shell rather than an overlay, so the stage's
   * area is the space actually left over and the media centres in it without a
   * magic offset that drifts whenever the rail changes width.
   */
  /*
   * THE RAIL IS COLUMN ONE, and in RTL that is the RIGHT edge.
   *
   * Written the other way round first - `1fr minmax(200px,240px)` with the rail
   * in column 2 - which put the navigation on the LEFT, because grid columns
   * are ordered along the INLINE axis and inline-start in RTL is the right-hand
   * side. The measurement caught it: rail at x=0 in a 1280 viewport.
   */
  /*
   * THE STAGE SPANS THE WHOLE VIEWPORT AND THE RAIL SITS OVER IT.
   *
   * Making the rail a grid COLUMN centred the media inside the space left over
   * (centre 520 in a 1280 viewport) rather than in the viewport itself (640).
   * The reference composition does the opposite and that is its whole point:
   * media centre 685.5 against a viewport centre of 681.5, with the navigation
   * on the right and a wide quiet gap absorbing the asymmetry.
   *
   * So the scroller spans edge to edge and the rail is fixed at inline-start -
   * which in RTL is the right edge. The media is narrow enough that it never
   * reaches the rail: at 1280 the stage is 460 wide, leaving ~290px of quiet
   * canvas on each side, which is the intentional emptiness the audit
   * describes rather than accidental dead space.
   */
  .feedshell {
    grid-template-columns: 1fr;
    /*
     * THE STAGE SIZE, DECLARED ONCE.
     *
     * The social rail is positioned against the media's edge, so it needs the
     * same number the stage is sized from. Writing that arithmetic twice is how
     * the rail ended up placed for a 508px stage while the stage was actually
     * 450px - a 46px gap where 16 was intended. One variable, two readers.
     */
    --fl-stage-h: calc(100dvh - 32px);
    /*
     * THE GUTTER FOLLOWS THE RAIL, because it IS the rail.
     *
     * This was the literal `260px`, which is the widest the navigation rail can
     * ever be plus a gap - and the rail is `clamp(200px, 18vw, 240px)`, so
     * below a 1334px viewport the reservation was larger than the thing being
     * reserved for. That is the same "one number, written twice" fault the
     * comment above this block already records once. One variable, and the
     * arithmetic is in one place.
     */
    --fl-stage-gutter: calc(clamp(200px, 18vw, 240px) + 20px);
    --fl-stage-w: min(
      calc(var(--fl-stage-h) * 9 / 16),
      calc(100vw - 2 * var(--fl-stage-gutter))
    );
    /*
     * THE HEIGHT THE STAGE ACTUALLY GETS, AND WHY IT IS A SECOND VARIABLE.
     *
     * `--fl-stage-w` is a `min()` of two terms, and which term wins decides
     * whether the frame is 9:16 or not. Above ~1030px the HEIGHT term wins and
     * the stage is exactly `--fl-stage-h` tall; below it the WIDTH term wins,
     * and a stage that keeps its full height while its width is clamped is not
     * a 9:16 frame any more - it is a column.
     *
     * MEASURED, at 768x1024, before this existed: a 248x992 stage, ratio 0.250,
     * with `object-fit: cover` throwing away 69% of every portrait photograph
     * in the feed. Nothing had ever seen it, because 768 is one of the four
     * widths Product names and is not one of the seven either existing feed
     * gate measures - `QA/product-feed-principles.js` is the first run at it.
     *
     * `w * 16/9` is <= `--fl-stage-h` by construction, so this never grows the
     * stage: at 1280 it resolves to exactly the height the stage already had.
     */
    --fl-stage-real-h: calc(var(--fl-stage-w) * 16 / 9);
  }

  .feedrail {
    position: fixed;
    inset-inline-start: 0;
    inset-block-start: 0;
    inline-size: minmax(200px, 240px);
    inline-size: clamp(200px, 18vw, 240px);
    z-index: 5;
    block-size: 100dvh;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-5) var(--space-4);
    overflow-y: auto;
    /*
     * FLAYVO NAVIGATION, ON A FLAYVO SURFACE.
     *
     * The rail inherited the shell's black and was drawn in white icons on it,
     * which is the reference product's navigation rather than this one's. The
     * destinations, the permissions and the order are unchanged; only the paint
     * is, and it is the same paint the header and every other surface uses.
     */
    background: var(--color-surface);
    color: var(--color-text);
    border-inline-start: 1px solid var(--color-border);
  }

  .feedstage { grid-column: 1; }

  .fitem__stage {
    /*
     * ASPECT-RATIO, not a percentage of the width.
     *
     * `inline-size: calc((100% - 32px) * 9/16)` looks like it derives the width
     * from the height and does not: a percentage in an inline-size resolves
     * against the containing block's WIDTH. In a 620px column that produced a
     * 331px stage. `aspect-ratio` is the property that actually ties one axis
     * to the other, so the stage stays 9:16 whatever the item height is.
     */
    /*
     * BOTH AXES FROM THE SAME VARIABLE, so the ratio cannot be lost.
     *
     * This was `block-size: calc(100% - 32px)` with `inline-size: auto`,
     * `aspect-ratio: 9/16` and `max-inline-size: var(--fl-stage-w)`, and that
     * set is self-contradictory the moment the max binds: a declared height and
     * a clamped width leave `aspect-ratio` nothing to decide, and the frame
     * silently becomes whatever is left. It held at 1280, where the max never
     * binds, and it is why nothing here was wrong at 1280.
     *
     * `--fl-stage-real-h` is `--fl-stage-w * 16/9` and is <= the old height by
     * construction, so at 1280 this is the same 432x768 stage as before and
     * below ~1030 it is a smaller 9:16 frame instead of a column.
     *
     * `aspect-ratio` stays as the declaration of intent for anyone reading the
     * rule, and now agrees with both sizes rather than competing with them.
     */
    inline-size: var(--fl-stage-w);
    block-size: var(--fl-stage-real-h);
    max-block-size: calc(100% - 32px);
    aspect-ratio: 9 / 16;
    border-radius: var(--radius-media);
  }

  /*
   * THE TOP SCRIM BELONGS TO THE MEDIA, SO IT ENDS WHERE THE MEDIA ENDS.
   *
   * The mode chrome spans the shell edge to edge, and it carries a dark
   * gradient so the labels stay readable over a bright photograph. Across the
   * full width of a light canvas that gradient is a black band over Flayvo's
   * own paper. Constrained to the stage, it is what it claims to be: a local
   * darkening of the top of the media.
   */
  .feedstage__modes {
    inline-size: min(100%, var(--fl-stage-w));
    margin-inline: auto;
    /*
     * AND IT STARTS WHERE THE MEDIA STARTS. The stage is centred in the item,
     * so the canvas above it is `(100% - real-h) / 2` - 16px at 1280, which is
     * where the chrome already sat, and 220px at 768, where a chrome pinned to
     * the shell's top edge would be a black gradient floating over warm paper
     * with no photograph anywhere near it.
     */
    inset-block-start: calc((100% - var(--fl-stage-real-h)) / 2);
    border-start-start-radius: var(--radius-media);
    border-start-end-radius: var(--radius-media);
  }

  .fitem__lower {
    padding-inline-end: calc(var(--space-5) + 64px);
    /* No tab bar at this width, so the allowance for it must not apply - it
       pushed the lower zone 8px past the viewport on desktop. */
    padding-block-end: var(--space-6);
  }
  /* Immediately beside the media, outside it, never on the food. */
  .frail {
    inset-inline-end: auto;
    /*
     * The media is centred in the viewport, so the rail is placed against that
     * centre rather than against a column: half the stage plus a 16px gap,
     * measured from the inline-START edge - which in RTL is the right - so the
     * rail lands immediately to the LEFT of the media.
     */
    inset-inline-start: calc(50% + var(--fl-stage-w) / 2 + 16px);
    /*
     * AND THE VERTICAL ANCHOR IS THE MEDIA'S OWN FOOT, for the same reason the
     * horizontal one is its own edge.
     *
     * `var(--space-8)` from the ITEM's foot was the same number as 16px above
     * the STAGE's foot for as long as the stage was always `100% - 32px` tall.
     * Once the stage is a 9:16 frame at every desktop width it is shorter than
     * the item below ~1030px, and a rail measured from the item would hang in
     * the canvas well beneath the food it belongs to.
     *
     * `(100% - real-h) / 2` is the canvas below the centred stage; at 1280 that
     * is 16px and the whole expression resolves to the 32px it resolved to
     * before, so nothing moves at the width this was designed at.
     */
    inset-block-end: calc((100% - var(--fl-stage-real-h)) / 2 + 16px);
  }

  .feedarrows {
    position: fixed;
    /*
     * inline-END, which in RTL is the physical LEFT.
     *
     * These were written at inline-START and that is where the navigation rail
     * is - fixed, full height, 240px wide, z-index 5 against the arrows' 4. The
     * controls rendered underneath the navigation and could not be clicked at
     * all; a Playwright click reported the rail intercepting the pointer for
     * thirty seconds. The fourth time an inline side has been the wrong one on
     * this surface, and the only reason it was caught is that the interaction
     * was measured rather than looked at - the arrows were plainly visible in
     * every screenshot, because the rail is translucent.
     *
     * The far physical-left edge is also the correct place on the merits: it is
     * the side opposite the navigation, clear of the social rail at ~322px, and
     * it mirrors the reference product across the RTL axis.
     */
    inset-inline-end: 16px;
    inset-block-start: 50%;
    transform: translateY(-50%);
    z-index: 4;
    display: grid;
    gap: var(--space-3);
  }
  /*
   * These sit on the application canvas, not on the media, so they are an
   * ordinary quiet Flayvo control rather than a translucent white disc borrowed
   * from a dark theme.
   */
  .feedarrows__btn {
    inline-size: 44px;
    block-size: 44px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: var(--shadow-1);
    cursor: pointer;
  }
  .feedarrows__btn:hover {
    background: var(--color-surface-sunken);
    color: var(--color-text-strong);
  }
}

/*
 * The arrows are revealed by the script, and the script owns it ALONE.
 *
 * A `[hidden]` element forced back to `display: grid` here is visible to the
 * eye and hidden to assistive technology at the same time, and it disagreed
 * with the script - which revealed the arrows for any fine pointer, including
 * a 390px viewport where this block does not apply and the base `.feedarrows`
 * rules do not exist. The result was two 44px buttons rendered in-flow on a
 * phone layout, on top of the bottom tab bar. One owner, one condition.
 */

/*
 * THE NO-PHOTO PLATE, ON THE DARK MEDIA SURFACE.
 *
 * The plate is a light clay wash, which is right on the browse card and wrong
 * here: the immersive Feed writes white chrome over its media, so a cream plate
 * left the For You / Following labels white-on-cream and drew a hard horizontal
 * edge where the lower scrim began. It is the same placeholder, in the surface's
 * own values - it must still be unmistakably NOT a photograph, and it still
 * says so in words.
 */
/*
 * THE NO-PHOTO STATE, ON THE MEDIA STAGE.
 *
 * Three wrong answers preceded this one, and each was wrong for a different
 * reason worth keeping.
 *
 *   1. The browse card's own bright clay wash. Correct everywhere else in the
 *      product; here it put a cream rectangle under white overlay type.
 *   2. Near-black. Legible, and it made an honest "no photograph" state look
 *      like a device that had failed to load.
 *   3. Warm paper inside the dark stage. Brand-correct and washed out: the
 *      lower zone's scrim runs to 0.85 black across the bottom 62% of the
 *      stage, so a pale plate faded into a hard dark band halfway down.
 *
 * The plate is INSIDE the media theatre, so it is a media surface: the clay end
 * of the ink ramp rather than paper, which keeps it warm, keeps the overlay
 * contract (white on media) intact for every item alike, and lets the scrim sit
 * on it the way it sits on a photograph.
 *
 * It is the same plate the rest of the product uses - rings, monogram, caption
 * in words - sized DOWN so it stops being the largest graphic in the
 * application. The shared component drew a 240px disc under a 132px letter in a
 * 509x904 stage; here the disc is a third of the width and the letter is a
 * title rather than a billboard. It still fills the media geometry, so paging
 * stays invariant.
 */
.fitem__media--plate .rh-plate {
  /*
   * F4. `--color-surface-inverse` MEANS "THE OTHER ONE", AND THE OTHER ONE
   * CHANGED.
   *
   * This asked for the inverse surface because, on warm paper, the inverse IS
   * the dark ink and the plate wanted to be dark. Under `data-direction="night"`
   * the inverse of a dark room is PAPER - correctly, by the token's own
   * definition - so the same declaration faded a 390x844 plate from clay to
   * `rgb(250, 248, 245)` and put the brightest surface in the theatre behind the
   * one item that has no photograph. The design review reported it as a blinding
   * "no picture" placeholder, and it was.
   *
   * `--color-bg` is what was meant all along: the ground of the room this plate
   * is standing in. It reads night here and would read paper if this plate ever
   * appeared on a paper surface, which is the behaviour the original line was
   * reaching for and did not get.
   */
  /*
   * `--media-placeholder`, WHICH IS WHAT THIS ALWAYS WAS.
   *
   * The first fix mixed a gradient here out of two other tokens and got the
   * right answer for the wrong reason: the plate is the ground behind a
   * photograph that does not exist, and the design system already has a token
   * that means exactly that. Under the night scope it resolves to the value
   * the review specified by hand - `linear-gradient(140deg, #3a2f28, #14100e)`
   * - and on any paper surface it resolves to the pale clay wash, so the plate
   * belongs to the room without this rule having to know which room it is in.
   */
  --plate-wash: var(--media-placeholder);
  /*
   * AND IT ACTUALLY FILLS THE STAGE, which the paragraph above has claimed
   * since it was written.
   *
   * `Plate` is rendered with `shape="feed"`, and `.rh-plate--feed` carries
   * `aspect-ratio: var(--media-ratio-feed)` - 4/5, which is correct for the
   * CARD media it was written for and wrong for a media theatre. Measured, at
   * 390x844: a 390x488 plate in an 844-tall stage, so an un-photographed item
   * showed the ground for 42% of the frame while a photographed one filled it.
   *
   * Nothing about that reads as a placeholder; it reads as a picture that
   * failed to load at the right size, which is the one thing the plate exists
   * not to look like. The ratio is released rather than overridden with another
   * number, because in here the FRAME decides the shape.
   */
  aspect-ratio: auto;
  block-size: 100%;
}
.fitem__media--plate .rh-plate::before {
  inline-size: min(34%, 190px);
  background: rgb(255 255 255 / 0.05);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.10);
}
.fitem__media--plate .rh-plate::after {
  inline-size: min(24%, 132px);
  border-color: rgb(255 255 255 / 0.14);
}
.fitem__media--plate .rh-plate__mark {
  font-size: clamp(28px, 12cqi, 64px);
  color: var(--rh-clay-300);
  opacity: 0.55;
}

/*
 * The caption sits ABOVE the lower content zone rather than at the plate's
 * foot: the overlay's scrim covers the bottom of the stage, and a caption
 * underneath it was being read through two layers of black.
 */
.fitem__media--plate .rh-plate__note {
  inset-block-end: 34%;
  background: transparent;
  color: var(--fl-media-on-muted);
  opacity: 1;
  font: var(--type-body-sm);
}

/* ==========================================================================
   THE SOCIAL RAIL, BY WHICH SURFACE IT IS ACTUALLY ON
   ==========================================================================

   The rail is the one component that changes LAYER with the viewport.

   On a phone the media fills the screen and the rail is over the food: layer 3,
   white ink, its own vignette, measured against the photograph underneath.

   On desktop the media is a stage in the middle of a warm canvas and the rail
   sits BESIDE it, on Flayvo's own paper: layer 1. White icons with a drop
   shadow there are a control borrowed from somebody else's dark product, and
   unreadable besides.

   This block comes after the base rail rules deliberately. An earlier version
   sat above them, lost the cascade at equal specificity, and rendered a solid
   black follow pill with black text inside it on the light canvas.
   ========================================================================== */

@media (min-width: 720px) {
  .frail__btn,
  .frail__follow {
    color: var(--color-text);
    /* The drop shadow exists to lift white glyphs off a photograph. On paper it
       is just a smudge. */
    filter: none;
  }
  .frail__count { color: var(--color-text-muted); }
  .frail__btn[aria-pressed="true"] { color: var(--color-accent); }
  .frail__btn:hover { color: var(--color-accent-hover); }

  .frail__follow {
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    color: var(--color-text-strong);
    box-shadow: var(--shadow-1);
  }
  .frail__follow[aria-pressed="true"] {
    color: var(--color-text-muted);
    background: var(--color-surface-sunken);
  }

  /* The avatar keeps a hairline so it reads as a portrait rather than as a
     floating crop on the canvas. */
  .frail__creator img,
  .frail__creator .rh-avatar {
    box-shadow: 0 0 0 1px var(--color-border);
  }
}

/* -- the rail is desktop only; the bottom tab bar already carries mobile --- */

.feedrail { display: none; }
/*
 * 720px, NOT 900 - the width where the tab bar stops.
 *
 * The rail appeared at 900 and the tab bar hides at 720, so between 720 and 899
 * the immersive Feed had NO primary navigation of any kind and a reader was
 * trapped inside /feed. Focused QA caught it at exactly 720. The two surfaces
 * hand over at one width because they are the same navigation, and a gap
 * between them is a lost product rather than a layout detail.
 */
@media (min-width: 720px) { .feedrail { display: flex; } }

.feedrail__brand {
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--color-accent-text);
  text-decoration: none;
  padding-block-end: var(--space-3);
}
.feedrail__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.feedrail__link,
.feedrail__sublink {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-block-size: var(--tap-min);
  padding-inline: var(--space-3);
  border-radius: var(--radius-input, 10px);
  color: var(--color-text-muted);
  text-decoration: none;
}
/* The current destination, in the accent family the whole product uses for
   "you are here" rather than a wash of white on black. */
.feedrail__link[aria-current="page"] {
  color: var(--color-accent-text);
  background: var(--color-accent-soft);
  font-weight: 700;
}
.feedrail__link:hover,
.feedrail__sublink:hover { color: var(--color-text-strong); }
.feedrail__summary { cursor: pointer; list-style: none; }
.feedrail__summary::-webkit-details-marker { display: none; }
.feedrail__sublist { list-style: none; margin: 0; padding-inline-start: var(--space-3); }

@media (prefers-reduced-motion: reduce) {
  .feedscroller { scroll-behavior: auto; }
}

/* ==========================================================================
   CONTEXTUAL SURFACES ON THE IMMERSIVE FEED
   ==========================================================================

   Comments, share and search are TASKS, not destinations: the reader's place in
   the queue survives them. Explore, a creator profile and Recipe Detail are
   destination changes and legitimately replace the Feed.

   None of this forks logic. The comments sheet keeps its existing dialog, its
   generation counter and its focus trap; only where it appears changed. Share
   calls `RecipeHub.shareLink`, the same contract the recipe page uses.
   ========================================================================== */

/* -- comments: a LEFT side panel on desktop -------------------------------- */

@media (min-width: 720px) {
  /*
   * Enters from the LEFT, which in RTL is the inline-END side - the same side
   * the social rail is on, because that is where the reader's attention already
   * is when they press the comment control.
   *
   * The Feed is NOT rebuilt: the panel is an overlay, so the active item, its
   * media and the scroll position are all still there behind it. Closing it
   * returns to the exact same item because nothing about the item ever changed.
   */
  .fsheet {
    /*
     * `flex-end`, because the panel must enter from the LEFT and in RTL the
     * flex END is the left. `flex-start` put it at x=896 in a 1280 viewport -
     * the right-hand side, on top of the navigation rail. The third time this
     * exact class of mistake appeared in this redesign, and the third time a
     * measurement caught it rather than a reading of the rule.
     */
    justify-content: flex-end;
    align-items: stretch;
  }

  .fsheet__panel {
    inline-size: min(100%, 24rem);
    max-block-size: 100dvh;
    block-size: 100dvh;
    border-radius: 0;
    animation: fsheet-slide 180ms ease-out;
  }

  /*
   * The scrim stays, but quieter than on mobile: the Feed behind it is the
   * context the reader is commenting ON, and blacking it out would make the
   * panel feel like a page rather than a conversation beside the dish.
   */
  .fsheet__scrim { background: rgb(0 0 0 / 0.35); }
}

/*
 * Top level, not nested in the media query above: the stylesheet parser the
 * content-fit gate uses reads one level of at-rule, and a `@keyframes` inside a
 * `@media` makes it refuse the entire sheet.
 */
@keyframes fsheet-slide {
  from { transform: translateX(-12px); opacity: .7; }
  to { transform: translateX(0); opacity: 1; }
}

/* -- share: a contextual modal over the Feed ------------------------------ */

.fshare {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.fshare[hidden] { display: none; }

.fshare__scrim {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.55);
}

/*
 * THE BACKDROP MAY BE DARK. THE PANEL IS A FLAYVO SURFACE.
 *
 * The modal was a dark card with light text, which belonged to the dark shell
 * it opened over. The shell is Warm Table now, and this is an application
 * surface wherever it opens: same paper, same ink, same elevation as every
 * other panel in the product.
 */
.fshare__panel {
  position: relative;
  inline-size: min(100%, 28rem);
  display: grid;
  gap: var(--space-3);
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--color-surface);
  color: var(--color-text);
  box-shadow: var(--shadow-3);
  text-align: start;
}

.fshare__title { margin: 0; font-size: var(--text-lg); color: var(--color-text-strong); }
.fshare__message { margin: 0; color: var(--color-text-muted); }

/*
 * The address itself, isolated LTR.
 *
 * A URL inside an RTL paragraph has its punctuation reordered by the bidi
 * algorithm - the reader is shown an address that is not the address. `dir`
 * is on the element in the markup; this makes it selectable and wrappable so a
 * long link can actually be copied by hand when the clipboard refused.
 */
.fshare__url {
  margin: 0;
  padding: var(--space-3);
  border-radius: var(--radius-input, 10px);
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  font-family: var(--font-mono, monospace);
  font-size: var(--text-sm);
  overflow-wrap: anywhere;
  user-select: all;
}

@media (prefers-reduced-motion: reduce) {
  .fsheet__panel { animation: none; }
}

/* ============================================================
 * MEMBER TEXT BREAKS WHERE IT HAS TO. `PRODUCT-I18N-01`, CP-24.
 * ============================================================
 *
 * Every selector below draws a string a MEMBER typed, and none of them could
 * break one. A recipe description carrying a long URL made the recipe page
 * 9,547px wide inside a 390px viewport - 9,157px of horizontal overflow, the
 * card boxes torn open by 319px, and controls sitting on top of each other -
 * and a 30-character username did the same thing to a feed card by 296px.
 *
 * THE RULE WAS ALREADY THE PRODUCT'S. `.u-pre`, `.ingredient-list li`,
 * `.step-list li`, `.person__name`, `.chat__item-name`, `.rh-adminrow__main`,
 * `.filepick__name` and `.fshare__url` all carry `overflow-wrap: anywhere`
 * already, and `.fshare__url` says in its own comment why. What was missing is
 * not the rule but its REACH: the surfaces above are where a member's own words
 * are drawn, and they were the ones without it. Two correct halves and no seam,
 * which is where this programme keeps finding things.
 *
 * `min-width: 0` is here for the same reason and is not decoration. A flex or
 * grid child's automatic minimum size is its CONTENT, so a long unbreakable run
 * pushes the TRACK wider than the container no matter what wrapping the text
 * itself is allowed - which is why the overflow above was measured on ancestors
 * that have no text of their own.
 *
 * It is `anywhere` rather than `break-word` on purpose: `break-word` does not
 * affect the intrinsic size used to lay the box out, so it fixes the paint and
 * leaves the 9,547px. Measured by `QA/product-i18n-dataset.js`.
 */
.rdetail__lead,
.memberhome__bio,
.card__title,
.card__title a,
.rcard__title,
.rcard__title a,
.rh-card__title,
.rh-card__title a,
.fitem__title,
.fitem__title a,
.fitem__creator,
.fitem__creator-name,
.fitem__source,
.rh-feedcard__title,
.rh-feedcard__title a,
.creator-card__name,
.creator-card__title,
.creator-card__handle,
.rdetail__creator-link,
.rdetail__creator-name,
.rdetail__handle,
.cook__ahead,
.cooksteps__text,
.cook__ingredient,
.card > .muted,
.card > .muted a,
.admin-list strong,
.admin-list strong a,
.admin-list .muted,
.rdetail__title,
.memberhome__name,
.memberhome__handle,
.memberhome__work-head,
.rh-section__title,
.creator-card__bio,
.chat__thread-title,
.cooklist__item,
.cooklist__label,
.cooklist__name,
.pricing__name,
/* A hashtag is a member's own word - up to the sixty characters the extractor
   permits, in one unbroken run by definition, since a token ends at the first
   character that is not part of a word. CP-35 joins the list rather than
   writing `overflow-wrap` beside the chip, which is what `CP24-01` was. */
.rh-tag,
/*
 * AND THE HEADING OF `/hashtags/:tag`, WHICH IS THE SAME WORD ONE SIZE LARGER.
 *
 * CP-35 added the CHIP to this list and stopped there, and the heading is the
 * one place the key is drawn at title size with nothing beside it. It went
 * unnoticed for a simple reason: the only tags this product had ever drawn
 * were the demo import's - `#פסטה`, `#onepan` - and every one of them fits.
 * The first sixty-character tag pushed the document 601px past a 390px
 * viewport, which is a page a member scrolls sideways to read.
 *
 * `[data-hashtag-heading]` rather than `.screen-head__title`, because it is
 * that attribute that says the heading is a MEMBER'S text. Every other screen
 * head in the product is the product's own copy, and joining them to a
 * member-text rule would make the rule mean less than it says.
 */
[data-hashtag-heading],
/*
 * AND THE THREE BOXES OF THE SHARE VIEW.
 *
 * `/r/:token` is read by a GUEST, and until the tag caption gave this walk a
 * reason to send one there it was the one product screen no width in this file
 * had ever measured - the walk's stations are all member surfaces. It draws a
 * member's title, byline and description with no member-text rule at all, and
 * the dataset put the document 1,287px past a 390px viewport: `1657>350` on the
 * title alone.
 *
 * It is the SAME three fields the recipe page draws through `.rdetail__title`
 * and `.rdetail__lead`, which have been on this list since CP-24. The share
 * view is a second component for the same content, so it needed the rule
 * repeating and nothing had said so.
 */
.shared-recipe__title,
.shared-recipe .recipe-byline,
.shared-recipe .recipe-description,
[data-share-url] {
  overflow-wrap: anywhere;
  min-width: 0;
}

/* ============================================================
 * HASHTAGS. `PRODUCT-HASHTAGS-01`, CP-35.
 * ============================================================
 *
 * One chip treatment for three surfaces - recipe detail, the feed stage and the
 * share view - because a tag that looked different on each would read as three
 * different kinds of object, and it is one object: the door the creator filed
 * the recipe behind.
 *
 * DIRECTION IS PER CHIP AND NEVER ON THE ROW. The demo import carries `#פסטה`
 * beside `#onepan` on purpose, and that pair is the case Product names. Each
 * chip is a flex ITEM, so it is its own block-level box: the row's order stays
 * the source order under `direction: rtl` while `dir="auto"` on the chip puts
 * each one's `#` on the side its own script wants. `isolate` is belt and braces
 * for the neutral `#` at the boundary between two chips of opposite direction.
 *
 * `min-inline-size: 0` and the member-text block at the foot of this file are
 * what stop a 60-character tag - the permitted maximum - widening the page. A
 * tag is a member's own word, so it joins that selector list like every other
 * surface that draws one; see the block's header for why `anywhere` rather than
 * `break-word`.
 *
 * THE FOCUS RING IS THE PRODUCT'S OWN and is deliberately not restyled here.
 * `:focus-visible` in `tokens.css` is one ring for the whole product, and
 * Product asks for "a visible focus state" rather than for a special one - a
 * chip with its own ring would be the fourth place that decision lives.
 */
.rh-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
  min-inline-size: 0;
}

.rh-tags__item { display: flex; min-inline-size: 0; }

/*
 * 44, NOT 48, AND THE PACKAGE RAISED IT ITSELF.
 *
 * Its `components/core/core.css` moved `.rh-tag--interactive` from 36 to 44
 * in the same delivery that set the control floor at 48, so a chip is
 * deliberately a smaller target than a button - it sits in a row of its
 * siblings and a 48px chip would break the line.
 */
.rh-tag {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap-chip);
  padding-inline: var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
  color: var(--color-accent-text);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  text-decoration: none;
  unicode-bidi: isolate;
  max-inline-size: 100%;
}

/* The `#` is quieter than the word, because the word is the information. */
.rh-tag__hash { opacity: 0.65; }

a.rh-tag:hover {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
  color: var(--color-accent-text);
}

/*
 * A tag a guest may read and may not follow - the share view. It has to look
 * like a fact rather than like a control somebody failed to style, so it loses
 * the border and the accent and keeps the shape.
 */
.rh-tag--static {
  border-color: transparent;
  color: var(--color-text-muted);
  font-weight: var(--weight-regular);
}

/* The overflow control. Quieter than a tag, because it names a count rather
   than a word somebody wrote. */
.rh-tag--more { color: var(--color-text-muted); }

/* --- the feed stage, where the chips sit on a photograph ------------------ */

.fitem__tags { margin-block-start: var(--space-2); }

/*
 * ON MEDIA, so the surface tokens above would vanish into a dark photograph.
 * The scrim behind the lower block is not guaranteed to be dark enough on its
 * own, which is why the chip carries its own translucent ground and a shadow
 * rather than relying on it.
 */
.fitem__tags .rh-tag {
  min-block-size: 32px;
  background: rgb(0 0 0 / 0.42);
  border-color: rgb(255 255 255 / 0.28);
  color: var(--fl-media-on, #fff);
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.6);
}
.fitem__tags a.rh-tag:hover {
  background: rgb(0 0 0 / 0.62);
  border-color: rgb(255 255 255 / 0.55);
  color: var(--fl-media-on, #fff);
}

/* --- the two document surfaces ------------------------------------------- */

.rdetail__tags,
.shared-recipe__tags { margin-block-start: var(--space-3); }

/* --- one tag's own screen ------------------------------------------------ */

/*
 * The active filter and the way out of it, on one line at every width. It wraps
 * rather than scrolls: the removal control is the thing a member who arrived by
 * accident is looking for, and a control that has scrolled out of a row is a
 * control that is not there.
 */
.hashtag-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  min-inline-size: 0;
}
.hashtag-filter__label { color: var(--color-text-muted); font-size: var(--text-xs); }
.hashtag-filter .rh-tag--active {
  background: var(--color-accent-soft);
  border-color: var(--color-accent);
}

/*
 * THE MAPPING DESK.
 *
 * One ingredient per card, and the card is ordered the way the decision is:
 * what the ingredient is, why it was refused, which dishes it blocks, and only
 * then the search. Putting the search first would invite somebody to map a key
 * before reading what it is blocking.
 */
.pricing-map__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
}

.pricing-map__name {
  margin: 0;
  font: var(--type-title-sm, var(--type-label));
  color: var(--color-text-strong);
}

.pricing-map__reason,
.pricing-map__blocked {
  margin: 0;
  font: var(--type-body-sm);
  text-wrap: pretty;
}

.pricing-map__blocked { margin-block-start: var(--space-2); }

.pricing-map__search { margin-block-start: var(--space-3); }

.pricing-map__search-row {
  display: flex;
  gap: var(--space-2);
  margin-block-start: var(--space-1);
}

.pricing-map__search-row input { flex: 1 1 auto; }

.pricing-map__results {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/*
 * The product, its facts and its two buttons. `flex-wrap` rather than a media
 * query: this panel sits in the admin column, and the width that decides is the
 * container's rather than the viewport's.
 */
.pricing-map__results li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
}

.pricing-map__product {
  font: var(--type-label);
  color: var(--color-text-strong);
}

.pricing-map__actions {
  display: flex;
  gap: var(--space-2);
  margin-inline-start: auto;
}

/* A list that is short because pricing FAILED is not a list that is short
   because the work is done, and the two must not look alike. */
.admin-note--warn {
  color: var(--color-text-strong);
  border-inline-start: 3px solid var(--color-warn, var(--color-border));
  padding-inline-start: var(--space-3);
}

/*
 * THE MARK BESIDE THE WORDMARK.
 *
 * The product carried no mark at all until 2026-08-25 - both shells shipped
 * `<link rel="icon" href="data:,">` and the brand was set in type alone. The
 * design system now supplies one, and it is drawn at the SAME optical weight as
 * the word rather than larger: a mark that outsizes its own wordmark reads as a
 * logo looking for a product.
 *
 * `flex` with a baseline-ish gap and no wrap, so the two never separate onto two
 * lines in a narrow header - a split brand is worse than a small one.
 */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  white-space: nowrap;
}

/*
 * Sized in `em` so it tracks whatever type size the header or the rail sets on
 * its brand link, rather than needing a second rule per surface.
 */
.brand__mark {
  inline-size: 1.35em;
  block-size: 1.35em;
  border-radius: var(--radius-sm, 6px);
  flex: 0 0 auto;
}

/* The wordmark keeps every rule it already had; this only stops the flex
   container from stretching it. */
/*
 * ============================================================================
 * THE WORDMARK, TO THE v3 PACKAGE'S OWN RULE
 * ============================================================================
 *
 * `assets/logo-v3/README.md`, rule 1, in full: "המילה Flayvo תמיד טקסט חי:
 * Heebo 700, דיו #2b211b (על כהה: #fdf7f3), direction:ltr; unicode-bidi:isolate.
 * לא חלק מה-SVG."
 *
 * Measured before this, on the three grounds it appears on:
 *
 *     paper header   #8e421f   weight 700     <- clay, not ink
 *     night rail     #ff8f78   weight 700     <- ember, not paper
 *     on the hero    #ffffff   weight 400     <- right family, wrong weight
 *
 * Three deviations, none of them deliberate: the word simply inherited
 * whatever its surroundings were painted in. On the night rail that meant the
 * night direction's accent, which is ember - a third colour for one word.
 *
 * THIS IS A VISIBLE CHANGE TO THE HEADER and it is worth naming: the wordmark
 * has read as clay through four review rounds. The v3 package specifies ink,
 * and a logo package's own usage rules outrank a colour nobody had written
 * down. It is stated here so the next capture is read as intended rather than
 * as drift.
 */
.brand__word {
  display: inline-block;
  font-weight: 700;
  color: #2b211b;
}

/*
 * On any dark ground - the night rail, the feed, a photograph - paper. One
 * value for all three rather than one per surface, which is what produced
 * three different words in the first place.
 */
[data-direction="night"] .brand__word,
.hero__top .brand__word,
.auth__media-logo .brand__word {
  color: #fdf7f3;
}

/*
 * AN INLINE LINK INSIDE A SENTENCE IS STILL SOMETHING A THUMB HAS TO HIT.
 *
 * `QA/design-system-conformance.js` measured every interactive element on ten
 * screens at two widths. Everything passed except one: the `הרשמה` link inside
 * "אין לכם חשבון? הרשמה" came back 22px tall on every 390 screen that carries
 * it - not clipped, not hidden, simply small.
 *
 * WCAG exempts inline links from target size, and the exemption is sound: you
 * cannot make a word inside a sentence 48px without wrecking the sentence. But
 * 22px is under even the 24px AA floor, and the fix costs nothing here.
 *
 * `padding-block` on an INLINE element extends the hit area without changing
 * the line box, so the sentence sets exactly as it did and the target grows to
 * 26px. `padding-inline` is deliberately absent: it would space the word away
 * from the question mark before it in an RTL line.
 */
.auth-inline-link,
.u-mt-3 > a,
.shared-recipe__foot > a {
  padding-block: var(--space-1);
  /* The hit area may overlap the line above; nothing there is interactive. */
  position: relative;
}

/*
 * A CHECKBOX IS HIT THROUGH ITS LABEL.
 *
 * The package states it exactly: "checkbox/radio - ה-label כולו הוא היעד
 * (min-height 48), לא הריבוע". Growing the box would make a 48px square where
 * a 16px one carries the meaning, and would give a mouse a worse target than
 * the words beside it.
 *
 * `:has()` rather than a class, so a label that gains a checkbox tomorrow
 * inherits the floor without anybody remembering this rule.
 */
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: var(--tap-min);
}

/*
 * A CARD TITLE IS THE CARD'S TARGET, AND IT WAS 25px.
 *
 * The package: "שם יוצר/מטא לחיץ - אזור המגע מורחב ל-48px בעזרת padding או
 * pseudo-element, בלי להגדיל את הטקסט". A recipe title is not meta, but it is
 * the same problem and takes the same answer: the box grows, the type does not.
 *
 * `inline-flex` rather than `block`, so a two-line title still wraps inside the
 * card instead of claiming the full row width.
 */
.card__title > a,
.rcard__title > a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap-min);
}

/*
 * THE LAST FOUR, EACH BY THE RULE THAT GOVERNS IT.
 *
 * A feed card title and a creator name are clickable text: the package says the
 * HIT AREA grows and the type does not. A chip keeps the 44 its own core.css
 * was raised to. The hashtag clear had no size rule at all.
 */
.fitem__title > a,
.creator-card__name > a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap-min);
}

.hashtag-filter__clear {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap-min);
}

/* The chip is a flex ITEM here, so the floor has to reach the item as well as
   the chip - a flex child can be squeezed below its own minimum. */
.rh-tags__item { min-block-size: var(--tap-chip); }
.rh-tags__item > .rh-tag { min-block-size: var(--tap-chip); }

/*
 * THE LAST THREE.
 *
 * `.fitem__source > a` is the credit link added this morning - clickable meta,
 * so the hit area grows by padding and the type does not.
 *
 * `.feedarrows__btn` is a standalone control on the desktop feed.
 */
.fitem__source > a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--tap-min);
}

.feedarrows__btn { min-block-size: var(--tap-min); }
